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 allow_origins_by_regex to cors plugin #3839

Merged
merged 11 commits into from
Mar 22, 2021
88 changes: 75 additions & 13 deletions apisix/plugins/cors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ local ngx = ngx
local plugin_name = "cors"
local str_find = core.string.find
local re_gmatch = ngx.re.gmatch
local re_compile = require("resty.core.regex").re_match_compile
local re_find = ngx.re.find
local ipairs = ipairs


local lrucache = core.lrucache.new({
Expand Down Expand Up @@ -73,7 +76,20 @@ local schema = {
"if you set this option to 'true', you can not use '*' for other options.",
type = "boolean",
default = false
}
},
allow_origins_by_regex = {
type = "array",
description =
"you can use regex to allow specific origins when no credentials," ..
"for example use [.*\\.test.com] to allow a.test.com and b.test.com",
items = {
type = "string",
minLength = 1,
maxLength = 4096,
},
minItems = 1,
uniqueItems = true,
},
}
}

Expand Down Expand Up @@ -121,6 +137,14 @@ function _M.check_schema(conf)
return false, "you can not set '*' for other option when 'allow_credential' is true"
end
end
if conf.allow_origins_by_regex then
for i, re_rule in ipairs(conf.allow_origins_by_regex) do
local ok, err = re_compile(re_rule, "j")
if not ok then
return false, err
end
end
end

return true
end
Expand Down Expand Up @@ -151,17 +175,8 @@ local function set_cors_headers(conf, ctx)
end
end


function _M.rewrite(conf, ctx)
if ctx.var.request_method == "OPTIONS" then
return 200
end
end


function _M.header_filter(conf, ctx)
local function process_with_allow_origins(conf, ctx, req_origin)
local allow_origins = conf.allow_origins
local req_origin = core.request.header(ctx, "Origin")
if allow_origins == "**" then
allow_origins = req_origin or '*'
end
Expand All @@ -179,8 +194,55 @@ function _M.header_filter(conf, ctx)
end
end

ctx.cors_allow_origins = allow_origins
set_cors_headers(conf, ctx)
return allow_origins
end

local function process_with_allow_origins_by_regex(conf, ctx, req_origin)
if conf.allow_origins_by_regex == nil then
return
end

if not conf.allow_origins_by_regex_rules_concat then
local allow_origins_by_regex_rules = {}
batman-ezio marked this conversation as resolved.
Show resolved Hide resolved
for i, re_rule in ipairs(conf.allow_origins_by_regex) do
allow_origins_by_regex_rules[i] = re_rule
end
conf.allow_origins_by_regex_rules_concat = core.table.concat(
allow_origins_by_regex_rules, "|")
end

-- core.log.warn("regex: ", conf.allow_origins_by_regex_rules_concat, "\n ")
local matched = re_find(req_origin, conf.allow_origins_by_regex_rules_concat, "jo")
if matched then
return req_origin
end
end


local function match_origins(req_origin, allow_origins)
return req_origin == allow_origins or allow_origins == '*'
end


function _M.rewrite(conf, ctx)
if ctx.var.request_method == "OPTIONS" then
return 200
end
end


function _M.header_filter(conf, ctx)
local req_origin = core.request.header(ctx, "Origin")
-- Try allow_origins first, if mismatched, try allow_origins_by_regex.
local allow_origins
allow_origins = process_with_allow_origins(conf, ctx, req_origin)
if not match_origins(req_origin, allow_origins) then
allow_origins = process_with_allow_origins_by_regex(conf, ctx, req_origin)
end
if allow_origins then
ctx.cors_allow_origins = allow_origins
set_cors_headers(conf, ctx)
end
end

return _M
1 change: 1 addition & 0 deletions docs/en/latest/plugins/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ title: cors
| expose_headers | string | optional | "*" | | Which headers are allowed to set in response when access cross-origin resource. Multiple value use `,` to split. |
| max_age | integer | optional | 5 | | Maximum number of seconds the results can be cached.. Within this time range, the browser will reuse the last check result. `-1` means no cache. Please note that the maximum value is depended on browser, please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#Directives) for details. |
| allow_credential | boolean | optional | false | | Enable request include credential (such as Cookie etc.). According to CORS specification, if you set this option to `true`, you can not use '*' for other options. |
| allow_origins_by_regex | array | optional | nil | | Use regex expressions to match which origin is allowed to enable CORS, for example, [".*\.test.com"] can use to match all subdomain of test.com |

> **Tips**
>
Expand Down
1 change: 1 addition & 0 deletions docs/zh/latest/plugins/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ title: cors
| expose_headers | string | 可选 | "*" | | 允许跨域访问时响应方携带哪些非 `CORS规范` 以外的 Header, 多个值使用 `,` 分割。 |
| max_age | integer | 可选 | 5 | | 浏览器缓存 CORS 结果的最大时间,单位为秒,在这个时间范围内浏览器会复用上一次的检查结果,`-1` 表示不缓存。请注意各个浏览器允许的的最大时间不同,详情请参考 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#Directives)。 |
| allow_credential | boolean | 可选 | false | | 是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 `true`,那么将不能在其他选项中使用 `*`。 |
| allow_origins_by_regex | array | 可选 | nil | | 使用正则表达式数组来匹配允许跨域访问的 Origin, 如[".*\.test.com"] 可以匹配任何test.com的子域名`*`。 |

> **提示**
>
Expand Down
169 changes: 169 additions & 0 deletions t/plugin/cors.t
Original file line number Diff line number Diff line change
Expand Up @@ -757,4 +757,173 @@ GET /t
--- response_body eval
qr/failed to check the configuration of plugin cors err: you can not/
--- no_error_log



=== TEST 28: set route (regex specified)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"cors": {
"allow_origins": "http://sub.domain.com,http://sub2.domain.com",
"allow_methods": "GET,POST",
"allow_headers": "headr1,headr2",
"expose_headers": "ex-headr1,ex-headr2",
"max_age": 50,
"allow_credential": true,
"allow_origins_by_regex":[".*\\.test.com"]
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]



=== TEST 29: regex specified
--- request
GET /hello HTTP/1.1
--- more_headers
Origin: http://a.test.com
resp-vary: Via
--- response_body
hello world
--- response_headers
Access-Control-Allow-Origin: http://a.test.com
Vary: Via, Origin
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: headr1,headr2
Access-Control-Expose-Headers: ex-headr1,ex-headr2
Access-Control-Max-Age: 50
Access-Control-Allow-Credentials: true
--- no_error_log
[error]



=== TEST 30: regex specified not match
--- request
GET /hello HTTP/1.1
--- more_headers
Origin: http://a.test2.com
resp-vary: Via
--- response_body
hello world
--- response_headers
Access-Control-Allow-Origin:
Access-Control-Allow-Methods:
Access-Control-Allow-Headers:
Access-Control-Expose-Headers:
Access-Control-Max-Age:
Access-Control-Allow-Credentials:
--- no_error_log
[error]



=== TEST 31: set route (multiple regex specified )
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"cors": {
"allow_origins": "http://sub.domain.com,http://sub2.domain.com",
"allow_methods": "GET,POST",
"allow_headers": "headr1,headr2",
"expose_headers": "ex-headr1,ex-headr2",
"max_age": 50,
"allow_credential": true,
"allow_origins_by_regex":[".*\\.test.com",".*\\.example.org"]
}
},
"upstream": {
"nodes": {
"127.0.0.1:1980": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]



=== TEST 32: multiple regex specified match
--- request
GET /hello HTTP/1.1
--- more_headers
Origin: http://foo.example.org
resp-vary: Via
--- response_body
hello world
--- response_headers
Access-Control-Allow-Origin: http://foo.example.org
Vary: Via, Origin
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: headr1,headr2
Access-Control-Expose-Headers: ex-headr1,ex-headr2
Access-Control-Max-Age: 50
Access-Control-Allow-Credentials: true
--- no_error_log
[error]



=== TEST 33: multiple regex specified not match
--- request
GET /hello HTTP/1.1
--- more_headers
Origin: http://foo.example.com
resp-vary: Via
--- response_body
hello world
--- response_headers
Access-Control-Allow-Origin:
Access-Control-Allow-Methods:
Access-Control-Allow-Headers:
Access-Control-Expose-Headers:
Access-Control-Max-Age:
Access-Control-Allow-Credentials:
--- no_error_log
[error]