-
Notifications
You must be signed in to change notification settings - Fork 170
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #487 from 3scale/cors-policy
Convert existing CORS module into a policy
- Loading branch information
Showing
2 changed files
with
248 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
--- CORS policy | ||
-- This policy enables CORS (Cross Origin Resource Sharing) request handling. | ||
-- The policy is configurable. Users can specify the values for the following | ||
-- headers in the response: | ||
-- - Access-Control-Allow-Headers | ||
-- - Access-Control-Allow-Methods | ||
-- - Access-Control-Allow-Origin | ||
-- - Access-Control-Allow-Credentials | ||
-- By default, those headers are set so all the requests are allowed. For | ||
-- example, if the request contains the 'Origin' header set to 'example.com', | ||
-- by default, 'Access-Control-Allow-Origin' in the response will be set to | ||
-- 'example.com' too. | ||
|
||
local policy = require('policy') | ||
local _M = policy.new('CORS Policy') | ||
|
||
local new = _M.new | ||
|
||
--- Initialize a CORS policy | ||
-- @tparam[opt] table config | ||
-- @field[opt] allow_headers Table with the allowed headers (e.g. Content-Type) | ||
-- @field[opt] allow_methods Table with the allowed methods (GET, POST, etc.) | ||
-- @field[opt] allow_origin Allowed origins (e.g. 'http://example.com', '*') | ||
-- @field[opt] allow_credentials Boolean | ||
function _M.new(config) | ||
local self = new() | ||
self.config = config or {} | ||
return self | ||
end | ||
|
||
local function set_access_control_allow_headers(allow_headers) | ||
local value = allow_headers or ngx.var.http_access_control_request_headers | ||
ngx.header['Access-Control-Allow-Headers'] = value | ||
end | ||
|
||
local function set_access_control_allow_methods(allow_methods) | ||
local value = allow_methods or ngx.var.http_access_control_request_method | ||
ngx.header['Access-Control-Allow-Methods'] = value | ||
end | ||
|
||
local function set_access_control_allow_origin(allow_origin, default) | ||
ngx.header['Access-Control-Allow-Origin'] = allow_origin or default | ||
end | ||
|
||
local function set_access_control_allow_credentials(allow_credentials) | ||
local value = allow_credentials | ||
if value == nil then value = 'true' end | ||
ngx.header['Access-Control-Allow-Credentials'] = value | ||
end | ||
|
||
local function set_cors_headers(config) | ||
local origin = ngx.var.http_origin | ||
if not origin then return end | ||
|
||
set_access_control_allow_headers(config.allow_headers) | ||
set_access_control_allow_methods(config.allow_methods) | ||
set_access_control_allow_origin(config.allow_origin, origin) | ||
set_access_control_allow_credentials(config.allow_credentials) | ||
end | ||
|
||
local function cors_preflight_response(config) | ||
set_cors_headers(config) | ||
ngx.status = 204 | ||
ngx.exit(ngx.status) | ||
end | ||
|
||
local function is_cors_preflight() | ||
return ngx.req.get_method() == 'OPTIONS' and | ||
ngx.var.http_origin and | ||
ngx.var.http_access_control_request_method | ||
end | ||
|
||
function _M:rewrite() | ||
if is_cors_preflight() then | ||
return cors_preflight_response(self.config) | ||
end | ||
end | ||
|
||
function _M:header_filter() | ||
set_cors_headers(self.config) | ||
end | ||
|
||
return _M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
use lib 't'; | ||
use TestAPIcastBlackbox 'no_plan'; | ||
|
||
repeat_each(1); | ||
run_tests(); | ||
|
||
__DATA__ | ||
=== TEST 1: CORS preflight request | ||
Returns 204 and sets the appropriate headers. | ||
--- configuration | ||
{ | ||
"services": [ | ||
{ | ||
"id": 42, | ||
"backend_version": 1, | ||
"backend_authentication_type": "service_token", | ||
"backend_authentication_value": "token-value", | ||
"proxy": { | ||
"policy_chain": [ | ||
{ "name": "policy.cors" }, | ||
{ "name": "apicast" } | ||
], | ||
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", | ||
"proxy_rules": [ | ||
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } | ||
] | ||
} | ||
} | ||
] | ||
} | ||
--- request | ||
OPTIONS / | ||
--- more_headers | ||
Origin: localhost | ||
Access-Control-Request-Method: GET | ||
--- error_code: 204 | ||
--- no_error_log | ||
[error] | ||
=== TEST 2: CORS actual request with default config | ||
This tests a CORS actual (not preflight) request. The only difference with a | ||
non-CORS request is that the CORS headers will be included in the response. | ||
In this test, we are not using a custom config for the CORS policy. This means | ||
that the response will contain the default CORS headers. By default, all of | ||
them (allow-headers, allow-methods, etc.) simply match the headers received in | ||
the request. So for example, if the request sets the 'Origin' header to | ||
'example.com' the response will set 'Access-Control-Allow-Origin' to | ||
'example.com'. | ||
--- configuration | ||
{ | ||
"services": [ | ||
{ | ||
"id": 42, | ||
"backend_version": 1, | ||
"backend_authentication_type": "service_token", | ||
"backend_authentication_value": "token-value", | ||
"proxy": { | ||
"policy_chain": [ | ||
{ "name": "policy.cors" }, | ||
{ "name": "apicast" } | ||
], | ||
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", | ||
"proxy_rules": [ | ||
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } | ||
] | ||
} | ||
} | ||
] | ||
} | ||
--- backend | ||
location /transactions/authrep.xml { | ||
content_by_lua_block { | ||
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value" | ||
local args = ngx.var.args | ||
if args == expected then | ||
ngx.exit(200) | ||
else | ||
ngx.log(ngx.ERR, expected, ' did not match: ', args) | ||
ngx.exit(403) | ||
end | ||
} | ||
} | ||
--- upstream | ||
location / { | ||
echo 'yay, api backend: $http_host'; | ||
} | ||
--- request | ||
GET /?user_key=value | ||
--- more_headers | ||
Origin: http://example.com | ||
Access-Control-Request-Method: GET | ||
Access-Control-Request-Headers: Content-Type | ||
--- response_body | ||
yay, api backend: test | ||
--- response_headers | ||
Access-Control-Allow-Headers: Content-Type | ||
Access-Control-Allow-Methods: GET | ||
Access-Control-Allow-Origin: http://example.com | ||
Access-Control-Allow-Credentials: true | ||
--- error_code: 200 | ||
--- no_error_log | ||
[error] | ||
=== TEST 3: CORS actual request with custom config | ||
This tests a CORS actual (not preflight) request. We use a custom config to set | ||
the CORS headers in the response. | ||
--- configuration | ||
{ | ||
"services": [ | ||
{ | ||
"id": 42, | ||
"backend_version": 1, | ||
"backend_authentication_type": "service_token", | ||
"backend_authentication_value": "token-value", | ||
"proxy": { | ||
"policy_chain": [ | ||
{ "name": "policy.cors", | ||
"configuration": { "allow_headers": [ "X-Custom-Header-1", "X-Custom-Header-2" ], | ||
"allow_methods": [ "POST", "GET", "OPTIONS" ], | ||
"allow_origin" : "*", | ||
"allow_credentials": false } }, | ||
{ "name": "apicast" } | ||
], | ||
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", | ||
"proxy_rules": [ | ||
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } | ||
] | ||
} | ||
} | ||
] | ||
} | ||
--- backend | ||
location /transactions/authrep.xml { | ||
content_by_lua_block { | ||
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value" | ||
local args = ngx.var.args | ||
if args == expected then | ||
ngx.exit(200) | ||
else | ||
ngx.log(ngx.ERR, expected, ' did not match: ', args) | ||
ngx.exit(403) | ||
end | ||
} | ||
} | ||
--- upstream | ||
location / { | ||
echo 'yay, api backend: $http_host'; | ||
} | ||
--- request | ||
GET /?user_key=value | ||
--- more_headers | ||
Origin: http://example.com | ||
Access-Control-Request-Method: GET | ||
Access-Control-Request-Headers: Content-Type | ||
--- response_body | ||
yay, api backend: test | ||
--- response_headers | ||
Access-Control-Allow-Headers: X-Custom-Header-1, X-Custom-Header-2 | ||
Access-Control-Allow-Methods: POST, GET, OPTIONS | ||
Access-Control-Allow-Origin: * | ||
Access-Control-Allow-Credentials: false | ||
--- error_code: 200 | ||
--- no_error_log | ||
[error] |