Skip to content

Commit

Permalink
Merge pull request #487 from 3scale/cors-policy
Browse files Browse the repository at this point in the history
Convert existing CORS module into a policy
  • Loading branch information
davidor authored Nov 21, 2017
2 parents 153a2ba + 9fee0ea commit 5e97083
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 0 deletions.
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]

0 comments on commit 5e97083

Please sign in to comment.