Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add forward-auth plugin #6037

Merged
merged 40 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
98d9671
feat: add forward auth plugin
bzp2010 Jan 6, 2022
da4c5ef
test: add get uri case
bzp2010 Jan 6, 2022
8525a76
feat: support client_headers
bzp2010 Jan 6, 2022
868fced
test: add forward-auth case
bzp2010 Jan 6, 2022
1060fcb
test: add plugin list
bzp2010 Jan 6, 2022
1a14ab1
feat: add default plugin list
bzp2010 Jan 6, 2022
5952350
fix: description typo
bzp2010 Jan 6, 2022
ea6d529
test: remove unused code
bzp2010 Jan 6, 2022
24882f8
fix: typo
bzp2010 Jan 6, 2022
eeaf575
docs: add forward-auth document
bzp2010 Jan 6, 2022
bf5ade0
feat: improve config check
bzp2010 Jan 6, 2022
e31e6da
docs: fix lint
bzp2010 Jan 6, 2022
4209f5a
Merge remote-tracking branch 'upstream/master' into feat-forward-auth
bzp2010 Jan 6, 2022
c2257c4
docs: add category
bzp2010 Jan 6, 2022
7ce1bac
fix: lint
bzp2010 Jan 6, 2022
e9d1fa6
fix: lint
bzp2010 Jan 6, 2022
18db978
feat: remove unused code
bzp2010 Jan 7, 2022
211c37f
docs: fix typo
bzp2010 Jan 7, 2022
8a853d3
feat: ignore client X headers
bzp2010 Jan 7, 2022
a84ddf9
docs: remove unused code
bzp2010 Jan 7, 2022
67b1a7e
docs: improve document
bzp2010 Jan 7, 2022
a4b05d9
fix: wrong code push
bzp2010 Jan 7, 2022
6c73cbd
test: improve too long codes
bzp2010 Jan 7, 2022
3df3ba5
test: fix typo
bzp2010 Jan 7, 2022
63811b5
test: add ignore client X-Forwarded headers case
bzp2010 Jan 7, 2022
ac3ae02
test: improve ignore client X-Forwarded headers case
bzp2010 Jan 7, 2022
8fef52c
feat: remove get uri
bzp2010 Jan 7, 2022
07eb935
chore: change http client error check
bzp2010 Jan 7, 2022
4fb97f7
test: split single line of long code
bzp2010 Jan 10, 2022
868fa43
feat: reverse request_headers and client_headers meaning
bzp2010 Jan 10, 2022
01f33f2
test: add headers logger
bzp2010 Jan 10, 2022
53f736b
test: add no upsteam_headers and client_headers case
bzp2010 Jan 10, 2022
6c6304c
test: remove headers logger
bzp2010 Jan 10, 2022
96a23ba
chore: remove default GET method
bzp2010 Jan 10, 2022
ba64f8f
chore: change error check
bzp2010 Jan 10, 2022
9565348
docs: update outdatad desc
bzp2010 Jan 10, 2022
beea582
test: extend APISIX generated headers check
bzp2010 Jan 10, 2022
c49890f
docs: add data definitions
bzp2010 Jan 11, 2022
a83e2b9
docs: add more data desc
bzp2010 Jan 11, 2022
8d11c22
docs: fix lint
bzp2010 Jan 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apisix/core/request.lua
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ function _M.get_path(ctx)
end


function _M.get_uri(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not add a common method that is only used by a place. Actually, I think we should remove the get_path too.

It is strange that get_path fetches $uri but get_uri fetches $request_uri. Look like it is a premature optimization that brings inconsistent names.

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I should just use ctx.var.request_uri instead of wrapping it? Can I merge the functionality of get_path into get_uri and add a with_args parameter indicating whether or not I need to carry queries.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't make thing complex!
The name of request_uri and uri already show the difference. There is no need to wrap them into a function which is rarely used. Premature optimization is the source of evil.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the get_path in the next PR. Better to get rid of it before the release, so that people won't ask why we use get_path in one place but use ctx.var.uri in the most elsewhere.

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed, I will remove get_path in a later PR.

if not ctx then
ctx = ngx.ctx.api_ctx
end

return ctx.var.request_uri or ''
end


function _M.get_http_version()
return ngx.req.http_version()
end
Expand Down
143 changes: 143 additions & 0 deletions apisix/plugins/forward-auth.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

local ipairs = ipairs
local core = require("apisix.core")
local http = require("resty.http")

local schema = {
type = "object",
properties = {
host = {type = "string"},
ssl_verify = {
type = "boolean",
default = true,
},
request_headers = {
type = "array",
default = {},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look like we can't support forward no headers from the client?
If this field is empty, all headers are forwarded. What about don't provide a default value? (Use empty array if no headers are need)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's rightfully so, and that's an oversight on my part. I have reversed the meaning of request_headers and client_headers, if the user does not set this parameter, APISIX will send nothing.

items = {type = "string"},
description = "client request header that will be sent to the authorization"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "client request header that will be sent to the authorization"
description = "client request header that will be sent to the authorization service"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

},
upstream_headers = {
type = "array",
default = {},
items = {type = "string"},
description = "authorization response header that will be sent to the upstream"
},
client_headers = {
type = "array",
default = {},
items = {type = "string"},
description = "authorization response header that will be sent to"
.. "the client when authorize failure"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. "the client when authorize failure"
.. "the client when authorizing failed"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

},
timeout = {
type = "integer",
minimum = 1,
maximum = 60000,
default = 3000,
description = "timeout in milliseconds",
},
keepalive = {type = "boolean", default = true},
keepalive_timeout = {type = "integer", minimum = 1000, default = 60000},
keepalive_pool = {type = "integer", minimum = 1, default = 5},
},
required = {"host"}
}


local _M = {
version = 0.1,
priority = 2002,
name = "forward-auth",
schema = schema,
}


function _M.check_schema(conf)
return core.schema.check(schema, conf)
end


function _M.access(conf, ctx)
local auth_headers = {
["X-Forwarded-Proto"] = core.request.get_scheme(ctx),
["X-Forwarded-Method"] = core.request.get_method(),
["X-Forwarded-Host"] = core.request.get_host(ctx),
["X-Forwarded-Uri"] = core.request.get_uri(ctx),
["X-Forwarded-For"] = core.request.get_remote_client_ip(ctx),
}

-- append headers that need to be get from the client request header
if #conf.request_headers > 0 then
for _, header in ipairs(conf.request_headers) do
auth_headers[header] = core.request.header(ctx, header)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better not trust the client's X-Forwarded-XX by default

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, because auth_headers has been set in our concern headers, after the judgment, the request header from the client can not override them, that's not trust the client header. L89-L91

local auth_headers = {
["X-Forwarded-Proto"] = core.request.get_scheme(ctx),
["X-Forwarded-Method"] = core.request.get_method(),
["X-Forwarded-Host"] = core.request.get_host(ctx),
["X-Forwarded-Uri"] = core.request.get_uri(ctx),
["X-Forwarded-For"] = core.request.get_remote_client_ip(ctx),
}
-- append headers that need to be get from the client request header
if #conf.request_headers > 0 then
for _, header in ipairs(conf.request_headers) do
if not auth_headers[header] then
auth_headers[header] = core.request.header(ctx, header)
end
end
else

end
else
auth_headers = core.table.merge(core.request.headers(), auth_headers)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because, merge the list of defined headers into the client request header, any key headers passed in by the client are overwritten, so I implement the untrusted client header.

end

local params = {
method = "GET",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the request method be mapped to the client request method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's needed unless we provide other special features for this, the implementation in other software is the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our or other forward-auth plugins, authentication-related data is sent via fixed requests, not POST data, so I think that's enough too.

ping @shuaijinchao

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, use GET as a fixed request method you should remove it because the default request method is GET

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, use GET as a fixed request method you should remove it because the default request method is GET

Oh, I get it. Modify will be made later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

headers = auth_headers,
keepalive = conf.keepalive,
ssl_verify = conf.ssl_verify
}

if conf.keepalive then
params.keepalive_timeout = conf.keepalive_timeout
params.keepalive_pool = conf.keepalive_pool
end

local httpc = http.new()
httpc:set_timeout(conf.timeout)

local res, err = httpc:request_uri(conf.host, params)

-- block by default when authorization service is unavailable
if not res or err then
core.log.error("failed to process forward auth, err: ", err)
return 403
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not res or err then
core.log.error("failed to process forward auth, err: ", err)
return 403
end
if not res then
core.log.error("failed to process forward auth, err: ", err)
return 403
end

Would it be better to judge this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how to do it really right, I noticed that in other plugin implementations there are those that use not res alone, those that use err alone and those that use not res or err, which one should we take?

local res, error = httpc:request_uri(conf.discovery, params)
if not res then
err = "Accessing discovery URL (" .. conf.discovery .. ") failed: " .. error

local res, err = httpc:request_uri(conf.function_uri, params)
if not res or err then
core.log.error("failed to process ", plugin_name, ", err: ", err)

local res, err = httpc:request_uri(uri, params)
if err then
core.log.error("FAIL REQUEST [ ",core.json.delay_encode(

I hope to get your guidance.
cc @spacewander

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read through each branch of https://github.com/api7/lua-resty-http/blob/b63a9bbf2a3361836b2322e3c689d6d7fd83ec55/lib/resty/http.lua#L900, look like there are only two cases:

  1. nil, err
  2. res, nil, while res is a table

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to the way it is usually used, I will check if err is not nil.

if err then
    xxx
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think there needs to make a rules that developers should follow to handle error checking (at least on the http client), the mix of different usages is confusing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we have to enforce it with code review. Maybe we can invest in linter in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be used

if res then
     err
end

instead of

if err then
     err
end

this way of writing may be more suitable for languages such as Go

the common exception function return in OpenResty is return nil, err, the first value is enough to know whether to handle err, and you can refer to more code and test cases in lua-resty-core, such as: https://github.com/openresty/lua-resty-core/blob/e5217414669c100b334940b250d5340911a02dd0/t/ctx.t#L133-L143

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


if res.status >= 300 then
local client_headers = {}

if #conf.client_headers > 0 then
for _, header in ipairs(conf.client_headers) do
client_headers[header] = res.headers[header]
end
else
client_headers = res.headers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, would be better to use allowlist? If no client_headers, no headers will be returned to the client. Some headers are not expected to be overridden, like Server / Date.

Copy link
Contributor Author

@bzp2010 bzp2010 Jan 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, changed

end

core.response.set_header(client_headers)
return res.status, res.body
end

-- append headers that need to be get from the auth response header
for _, header in ipairs(conf.upstream_headers) do
local header_value = res.headers[header]
if header_value then
core.request.set_header(ctx, header, header_value)
end
end
end


return _M
1 change: 1 addition & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ plugins: # plugin list (sorted by priority)
- jwt-auth # priority: 2510
- key-auth # priority: 2500
- consumer-restriction # priority: 2400
- forward-auth # priority: 2002
- opa # priority: 2001
- authz-keycloak # priority: 2000
#- error-log-logger # priority: 1091
Expand Down
3 changes: 2 additions & 1 deletion docs/en/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"plugins/hmac-auth",
"plugins/authz-casbin",
"plugins/ldap-auth",
"plugins/opa"
"plugins/opa",
"plugins/forward-auth"
]
},
{
Expand Down
135 changes: 135 additions & 0 deletions docs/en/latest/plugins/forward-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: forward-auth
---

<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->

## Summary

- [**Description**](#description)
- [**Attributes**](#attributes)
- [**Example**](#example)

## Description

The `forward-auth` plugin implement a classic external authentication model. We can implement a custom error return or user redirection to the authentication page if the authentication fails.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `forward-auth` plugin implement a classic external authentication model. We can implement a custom error return or user redirection to the authentication page if the authentication fails.
The `forward-auth` plugin implements a classic external authentication model. We can implement a custom error return or user redirection to the authentication page if the authentication fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


Forward Auth cleverly moves the authentication and authorization logic to a dedicated external service, where the gateway forwards the user's request to the authentication service and blocks the original request and replaces the result when the authentication service responds with a non-20x status.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Forward Auth cleverly moves the authentication and authorization logic to a dedicated external service, where the gateway forwards the user's request to the authentication service and blocks the original request and replaces the result when the authentication service responds with a non-20x status.
Forward Auth cleverly moves the authentication and authorization logic to a dedicated external service, where the gateway forwards the user's request to the authentication service and blocks the original request, and replaces the result when the authentication service responds with a non-2xx status.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


## Attributes

| Name | Type | Requirement | Default | Valid | Description |
| -- | -- | -- | -- | -- | -- |
| host | string | required | | | Authorization service host (eg. https://localhost:9188) |
| ssl_verify | boolean | optional | true | | Whether to verify the certificate |
| request_headers | array[string] | optional | | | `client` request header that will be sent to the `authorization` service |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's doc the behavior when this field is empty

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

| upstream_headers | array[string] | optional | | | `authorization` service response header that will be sent to the `upstream` |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

| client_headers | array[string] | optional | | | `authorization` response header that will be sent to the `client` when authorize failure |
| timeout | integer | optional | 3000ms | [1, 60000]ms | Authorization service HTTP call timeout |
| keepalive | boolean | optional | true | | HTTP keepalive |
| keepalive_timeout | integer | optional | 60000ms | [1000, ...]ms | keepalive idle timeout |
| keepalive_pool | integer | optional | 5 | [1, ...]ms | Connection pool limit |

## Example

First, you need to setup an external authorization service. Here is an example of using Apache APISIX's serverless plugin to mock.

```shell
$ curl -X PUT 'http://127.0.0.1:9080/apisix/admin/routes/auth' \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
-H 'Content-Type: application/json' \
-d '{
"uri": "/auth",
"plugins": {
"serverless-pre-function": {
"phase": "rewrite",
"functions": [
"return function (conf, ctx) local core = require(\"apisix.core\"); local authorization = core.request.header(ctx, \"Authorization\"); if authorization == \"123\" then core.response.exit(200); elseif authorization == \"321\" then core.response.set_header(\"X-User-ID\", \"i-am-user\"); core.response.exit(200); else core.response.set_header(\"Location\", \"http://example.com/auth\"); core.response.exit(403); end end"
]
}
},
"upstream": {
"nodes": {},
"scheme": "https",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove useless field

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

"type": "roundrobin"
}
}'
```

Next, we create a route for testing.

```shell
$ curl -X PUT http://127.0.0.1:9080/apisix/admin/routes/1
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
-d '{
"uri": "/headers",
"plugins": {
"forward-auth": {
"host": "http://127.0.0.1:9080/auth",
"request_headers": ["Authorization"],
"upstream_headers": ["X-User-ID"],
"client_headers": ["Location"]
}
},
"upstream": {
"nodes": {
"httpbin.org:80": 1
},
"type": "roundrobin"
}
}'
```

We can perform the following three tests.

1. **request_headers** Send Authorization header from `client` to `authorization` service

```shell
$ curl http://127.0.0.1:9080/headers -H 'Authorization: 123'
{
"headers": {
"Authorization": "123",
"Next": "More-headers"
}
}
```

2. **upstream_headers** Send `authorization` service response header to the `upstream`

```shell
$ curl http://127.0.0.1:9080/headers -H 'Authorization: 321'
{
"headers": {
"Authorization": "321",
"X-User-ID": "i-am-user",
"Next": "More-headers"
}
}
```

3. **client_headers** Send `authorization` service response header to `client` when authorize failure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. **client_headers** Send `authorization` service response header to `client` when authorize failure
3. **client_headers** Send `authorization` service response header to `client` when authorizing failed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


```shell
$ curl -i http://127.0.0.1:9080/headers
HTTP/1.1 403 Forbidden
Location: http://example.com/auth
```

Finally, you can disable the `forward-auth` plugin by removing it from the route.
1 change: 1 addition & 0 deletions t/admin/plugins.t
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ basic-auth
jwt-auth
key-auth
consumer-restriction
forward-auth
opa
authz-keycloak
proxy-mirror
Expand Down
27 changes: 27 additions & 0 deletions t/core/request.t
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,30 @@ GET /hello1/test?a=b&b=a
/hello1/test
--- no_error_log
[error]



=== TEST 15: get_uri
--- config
location /hello {
content_by_lua_block {
local core = require("apisix.core")
local ngx_ctx = ngx.ctx
local api_ctx = ngx_ctx.api_ctx
if api_ctx == nil then
api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
ngx_ctx.api_ctx = api_ctx
end

core.ctx.set_vars_meta(api_ctx)

local path = core.request.get_uri(ngx.ctx.api_ctx)
ngx.say(path)
}
}
--- request
GET /hello/test?a=b&b=a
--- response_body
/hello/test?a=b&b=a
--- no_error_log
[error]
Loading