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

Convert existing CORS module into a policy #487

Merged
merged 4 commits into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 83 additions & 0 deletions apicast/src/policy/cors.lua
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
165 changes: 165 additions & 0 deletions t/apicast-policy-cors.t
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]