Skip to content
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ global
lua-load /path/to/cors.lua
```

In your `frontend` or `listen` section, capture the client's *Origin* request header by adding `http-request lua.cors`:
In your `frontend` or `listen` section, capture the client's *Origin* request header by adding `http-request lua.cors` The first parameter is a comma-delimited list of HTTP methods that can be used. The second parameter is comma-delimited list of origins that are permitted to call your service.

```
http-request lua.cors
http-request lua.cors "GET,PUT,POST" "example.com,localhost,localhost:8080"
```

Within the same section, invoke the `http-response lua.cors` action. The first parameter is a a comma-delimited list of HTTP methods that can be used. The second parameter is comma-delimited list of origins that are permitted to call your service.
Within the same section, invoke the `http-response lua.cors` action to attach CORS headers to responses from backend servers.

```
http-response lua.cors "GET,PUT,POST" "example.com,localhost,localhost:8080"
http-response lua.cors
```

You can also whitelist all domains by setting the second parameter to an asterisk:

```
http-response lua.cors "GET,PUT,POST" "*"
http-request lua.cors "GET,PUT,POST" "*"
```
2 changes: 1 addition & 1 deletion example/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ services:
- "name=server1"

haproxy:
image: haproxytech/haproxy-ubuntu:2.0
image: haproxytech/haproxy-ubuntu:2.2
volumes:
- "./haproxy/haproxy.cfg:/etc/haproxy/haproxy.cfg"
- "./haproxy/cors.lua:/etc/haproxy/cors.lua"
Expand Down
144 changes: 108 additions & 36 deletions example/haproxy/cors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,59 +20,131 @@ function contains(items, test_str)
return false
end

-- When invoked during a request, captures the Origin header if present
-- and stores it in a private variable.
function cors_request(txn)
local headers = txn.http:req_get_headers()
local origin = headers["origin"]

-- If the given origin is found within the allowed_origins string, it is returned. Otherwise, nil is returned.
-- origin: The value from the 'origin' request header
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
function get_allowed_origin(origin, allowed_origins)
if origin ~= nil then
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
txn:set_priv(headers["origin"][0])
local allowed_origins = core.tokenize(allowed_origins, ",")

-- Strip whitespace
for index, value in ipairs(allowed_origins) do
allowed_origins[index] = value:gsub("%s+", "")
end

if contains(allowed_origins, "*") then
return "*"
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
return origin
end
end

return nil
end

-- When invoked during a response, sets CORS headers so that the browser
-- can read the response from permitted domains.
-- txn: The current transaction object that gives access to response properties.
-- Adds headers for CORS preflight request and then attaches them to the response
-- after it comes back from the server. This works with versions of HAProxy prior to 2.2.
-- The downside is that the OPTIONS request must be sent to the backend server first and can't
-- be intercepted and returned immediately.
-- txn: The current transaction object that gives access to response properties
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
function preflight_request_ver1(txn, allowed_methods)
core.Debug("CORS: preflight request received")
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
txn.http:res_add_header("Access-Control-Max-Age", 600)
core.Debug("CORS: attaching allowed methods to response")
end

-- Add headers for CORS preflight request and then returns a 204 response.
-- The 'reply' function used here is available in HAProxy 2.2+. It allows HAProxy to return
-- a reply without contacting the server.
-- txn: The current transaction object that gives access to response properties
-- origin: The value from the 'origin' request header
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
function preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
core.Debug("CORS: preflight request received")

local reply = txn:reply()
reply:set_status(204, "No Content")
reply:add_header("Content-Type", "text/html")
reply:add_header("Access-Control-Allow-Methods", allowed_methods)
reply:add_header("Access-Control-Max-Age", 600)

local allowed_origin = get_allowed_origin(origin, allowed_origins)

if allowed_origin == nil then
core.Debug("CORS: " .. origin .. " not allowed")
else
core.Debug("CORS: " .. origin .. " allowed")
reply:add_header("Access-Control-Allow-Origin", allowed_origin)
end

core.Debug("CORS: Returning reply to preflight request")
txn:done(reply)
end

-- When invoked during a request, captures the origin header if present and stores it in a private variable.
-- If the request is OPTIONS and it is a supported version of HAProxy, returns a preflight request reply.
-- Otherwise, the preflight request header is added to the response after it has returned from the server.
-- txn: The current transaction object that gives access to response properties
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
function cors_response(txn, allowed_methods, allowed_origins)
function cors_request(txn, allowed_methods, allowed_origins)
local headers = txn.http:req_get_headers()
local origin = headers["origin"][0]

local transaction_data = {}

if origin ~= nil then
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
transaction_data["origin"] = origin
end

transaction_data["allowed_methods"] = allowed_methods
transaction_data["allowed_origins"] = allowed_origins

txn:set_priv(transaction_data)

local method = txn.sf:method()
local origin = txn:get_priv()

-- add headers for CORS preflight request
if method == "OPTIONS" then
core.Debug("CORS: preflight request OPTIONS")
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
txn.http:res_set_header("Allow", allowed_methods)
txn.http:res_add_header("Access-Control-Max-Age", 600)
transaction_data["method"] = method

if method == "OPTIONS" and txn.reply ~= nil then
preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
end
end

-- When invoked during a response, sets CORS headers so that the browser can read the response from permitted domains.
-- txn: The current transaction object that gives access to response properties.
function cors_response(txn)
local transaction_data = txn:get_priv()
local origin = transaction_data["origin"]
local allowed_origins = transaction_data["allowed_origins"]
local allowed_methods = transaction_data["allowed_methods"]
local method = transaction_data["method"]

-- Always vary on the Origin
txn.http:res_add_header("Vary", "Accept-Encoding,Origin")

-- Bail if client did not send an Origin
if origin == nil or origin == '' then
return
end

local allowed_origins = core.tokenize(allowed_origins, ",")

-- Strip whitespace
for index, value in ipairs(allowed_origins) do
allowed_origins[index] = value:gsub("%s+", "")
end
local allowed_origin = get_allowed_origin(origin, allowed_origins)

if contains(allowed_origins, "*") then
core.Debug("CORS: " .. "* allowed")
txn.http:res_add_header("Access-Control-Allow-Origin", "*")
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
core.Debug("CORS: " .. origin .. " allowed")
txn.http:res_add_header("Access-Control-Allow-Origin", origin)
txn.http:res_add_header("Vary", "Accept-Encoding,Origin")
else
if allowed_origin == nil then
core.Debug("CORS: " .. origin .. " not allowed")
else
if method == "OPTIONS" and txn.reply == nil then
preflight_request_ver1(txn, allowed_methods)
end

core.Debug("CORS: " .. origin .. " allowed")
txn.http:res_add_header("Access-Control-Allow-Origin", allowed_origin)
end
end

-- Register the actions with HAProxy
core.register_action("cors", {"http-req"}, cors_request, 0)
core.register_action("cors", {"http-res"}, cors_response, 2)
core.register_action("cors", {"http-req"}, cors_request, 2)
core.register_action("cors", {"http-res"}, cors_response, 0)
4 changes: 2 additions & 2 deletions example/haproxy/haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ listen api
bind :8080

# Invoke the CORS service on the request to capture the Origin header
http-request lua.cors
http-request lua.cors "GET,PUT,POST", "localhost"

# Invoke the CORS service on the response to add CORS headers
http-response lua.cors "GET,PUT,POST" "localhost"
http-response lua.cors
server s1 server1:80 check
139 changes: 103 additions & 36 deletions lib/cors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,108 @@ function contains(items, test_str)
return false
end

-- When invoked during a request, captures the Origin header if present
-- and stores it in a private variable.
function cors_request(txn)
local headers = txn.http:req_get_headers()
local origin = headers["origin"]

-- If the given origin is found within the allowed_origins string, it is returned. Otherwise, nil is returned.
-- origin: The value from the 'origin' request header
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
function get_allowed_origin(origin, allowed_origins)
if origin ~= nil then
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
txn:set_priv(headers["origin"][0])
local allowed_origins = core.tokenize(allowed_origins, ",")

-- Strip whitespace
for index, value in ipairs(allowed_origins) do
allowed_origins[index] = value:gsub("%s+", "")
end

if contains(allowed_origins, "*") then
return "*"
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
return origin
end
end

return nil
end

-- Add headers for CORS preflight request
function preflight_request(txn, method, allowed_methods)
if method == "OPTIONS" then
core.Debug("CORS: preflight request OPTIONS")
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
txn.http:res_add_header("Access-Control-Max-Age", 600)
-- Adds headers for CORS preflight request and then attaches them to the response
-- after it comes back from the server. This works with versions of HAProxy prior to 2.2.
-- The downside is that the OPTIONS request must be sent to the backend server first and can't
-- be intercepted and returned immediately.
-- txn: The current transaction object that gives access to response properties
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
function preflight_request_ver1(txn, allowed_methods)
core.Debug("CORS: preflight request received")
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
txn.http:res_add_header("Access-Control-Max-Age", 600)
core.Debug("CORS: attaching allowed methods to response")
end

-- Add headers for CORS preflight request and then returns a 204 response.
-- The 'reply' function used here is available in HAProxy 2.2+. It allows HAProxy to return
-- a reply without contacting the server.
-- txn: The current transaction object that gives access to response properties
-- origin: The value from the 'origin' request header
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
function preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
core.Debug("CORS: preflight request received")

local reply = txn:reply()
reply:set_status(204, "No Content")
reply:add_header("Content-Type", "text/html")
reply:add_header("Access-Control-Allow-Methods", allowed_methods)
reply:add_header("Access-Control-Max-Age", 600)

local allowed_origin = get_allowed_origin(origin, allowed_origins)

if allowed_origin == nil then
core.Debug("CORS: " .. origin .. " not allowed")
else
core.Debug("CORS: " .. origin .. " allowed")
reply:add_header("Access-Control-Allow-Origin", allowed_origin)
end

core.Debug("CORS: Returning reply to preflight request")
txn:done(reply)
end

-- When invoked during a response, sets CORS headers so that the browser
-- can read the response from permitted domains.
-- txn: The current transaction object that gives access to response properties.
-- When invoked during a request, captures the origin header if present and stores it in a private variable.
-- If the request is OPTIONS and it is a supported version of HAProxy, returns a preflight request reply.
-- Otherwise, the preflight request header is added to the response after it has returned from the server.
-- txn: The current transaction object that gives access to response properties
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
function cors_response(txn, allowed_methods, allowed_origins)
function cors_request(txn, allowed_methods, allowed_origins)
local headers = txn.http:req_get_headers()
local origin = headers["origin"][0]

local transaction_data = {}

if origin ~= nil then
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
transaction_data["origin"] = origin
end

transaction_data["allowed_methods"] = allowed_methods
transaction_data["allowed_origins"] = allowed_origins

txn:set_priv(transaction_data)

local method = txn.sf:method()
local origin = txn:get_priv()
transaction_data["method"] = method

if method == "OPTIONS" and txn.reply ~= nil then
preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
end
end

-- When invoked during a response, sets CORS headers so that the browser can read the response from permitted domains.
-- txn: The current transaction object that gives access to response properties.
function cors_response(txn)
local transaction_data = txn:get_priv()
local origin = transaction_data["origin"]
local allowed_origins = transaction_data["allowed_origins"]
local allowed_methods = transaction_data["allowed_methods"]
local method = transaction_data["method"]

-- Always vary on the Origin
txn.http:res_add_header("Vary", "Accept-Encoding,Origin")
Expand All @@ -58,26 +131,20 @@ function cors_response(txn, allowed_methods, allowed_origins)
return
end

local allowed_origins = core.tokenize(allowed_origins, ",")

-- Strip whitespace
for index, value in ipairs(allowed_origins) do
allowed_origins[index] = value:gsub("%s+", "")
end
local allowed_origin = get_allowed_origin(origin, allowed_origins)

if contains(allowed_origins, "*") then
core.Debug("CORS: " .. "* allowed")
txn.http:res_add_header("Access-Control-Allow-Origin", "*")
preflight_request(txn, method, allowed_methods)
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
core.Debug("CORS: " .. origin .. " allowed")
txn.http:res_add_header("Access-Control-Allow-Origin", origin)
preflight_request(txn, method, allowed_methods)
else
if allowed_origin == nil then
core.Debug("CORS: " .. origin .. " not allowed")
else
if method == "OPTIONS" and txn.reply == nil then
preflight_request_ver1(txn, allowed_methods)
end

core.Debug("CORS: " .. origin .. " allowed")
txn.http:res_add_header("Access-Control-Allow-Origin", allowed_origin)
end
end

-- Register the actions with HAProxy
core.register_action("cors", {"http-req"}, cors_request, 0)
core.register_action("cors", {"http-res"}, cors_response, 2)
core.register_action("cors", {"http-req"}, cors_request, 2)
core.register_action("cors", {"http-res"}, cors_response, 0)