Skip to content

Commit

Permalink
feat: add allow_origins_by_regex to cors plugin (#3839)
Browse files Browse the repository at this point in the history
  • Loading branch information
batman-ezio authored Mar 22, 2021
1 parent 7ae3020 commit bb95f7a
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 13 deletions.
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 = {}
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]

0 comments on commit bb95f7a

Please sign in to comment.