second commit
This commit is contained in:
+247
@@ -0,0 +1,247 @@
|
||||
--
|
||||
-- Cross-origin Request Sharing (CORS) implementation for HAProxy Lua host
|
||||
--
|
||||
-- CORS RFC:
|
||||
-- https://www.w3.org/TR/cors/
|
||||
--
|
||||
-- Copyright (c) 2019. Nick Ramirez <nramirez@haproxy.com>
|
||||
-- Copyright (c) 2019. HAProxy Technologies, LLC.
|
||||
|
||||
local M={}
|
||||
|
||||
-- Loops through array to find the given string.
|
||||
-- items: array of strings
|
||||
-- test_str: string to search for
|
||||
function contains(items, test_str)
|
||||
for _,item in pairs(items) do
|
||||
if item == test_str then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
M.wildcard_origin_allowed = function(allowed_origins)
|
||||
if contains(allowed_origins, "*") then
|
||||
return "*"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
M.specifies_scheme = function(s)
|
||||
return string.find(s, "^%a+://") ~= nil
|
||||
end
|
||||
|
||||
M.specifies_generic_scheme = function(s)
|
||||
return string.find(s, "^//") ~= nil
|
||||
end
|
||||
|
||||
M.begins_with_dot = function(s)
|
||||
return string.find(s, "^%.") ~= nil
|
||||
end
|
||||
|
||||
M.trim = function(s)
|
||||
return s:gsub("%s+", "")
|
||||
end
|
||||
|
||||
M.build_pattern = function(pattern)
|
||||
-- remove spaces
|
||||
pattern = M.trim(pattern)
|
||||
|
||||
if pattern ~= nil and pattern ~= '' then
|
||||
-- if there is no scheme and the pattern does not begin with a dot,
|
||||
-- add the generic scheme to the beginning of the pattern
|
||||
if M.specifies_scheme(pattern) == false and M.specifies_generic_scheme(pattern) == false and M.begins_with_dot(pattern) == false then
|
||||
pattern = "//" .. pattern
|
||||
end
|
||||
|
||||
-- escape dots and dashes in pattern
|
||||
pattern = pattern:gsub("([%.%-])", "%%%1")
|
||||
|
||||
-- an asterisk for the port means allow all ports
|
||||
if string.find(pattern, "[:]+%*$") ~= nil then
|
||||
pattern = pattern:gsub("[:]+%*$", "[:]+[0-9]+")
|
||||
end
|
||||
|
||||
-- append end character
|
||||
pattern = pattern .. "$"
|
||||
return pattern
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 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_origins: Comma-delimited list of allowed origins. (e.g. localhost,https://localhost:8080,//test.com)
|
||||
-- e.g. localhost : allow http(s)://localhost
|
||||
-- e.g. //localhost : allow http(s)://localhost
|
||||
-- e.g. https://mydomain.com : allow only HTTPS of mydomain.com
|
||||
-- e.g. http://mydomain.com : allow only HTTP of mydomain.com
|
||||
-- e.g. http://mydomain.com:8080 : allow only HTTP of mydomain.com from port 8080
|
||||
-- e.g. //mydomain.com : allow only http(s)://mydomain.com
|
||||
-- e.g. .mydomain.com : allow ALL subdomains of mydomain.com from ALL source ports
|
||||
-- e.g. .mydomain.com:443 : allow ALL subdomains of mydomain.com from default HTTPS source port
|
||||
--
|
||||
-- e.g. ".mydomain.com:443, //mydomain.com:443, //localhost"
|
||||
-- allows all subdomains and main domain of mydomain.com only for HTTPS from default HTTPS port and allows
|
||||
-- all HTTP and HTTPS connections from ALL source port for localhost
|
||||
--
|
||||
M.get_allowed_origin = function(origin, allowed_origins)
|
||||
if origin ~= nil then
|
||||
-- if wildcard (*) is allowed, return it, which allows all origins
|
||||
wildcard_origin = M.wildcard_origin_allowed(allowed_origins)
|
||||
if wildcard_origin ~= nil then
|
||||
return wildcard_origin
|
||||
end
|
||||
|
||||
for index, allowed_origin in ipairs(allowed_origins) do
|
||||
pattern = M.build_pattern(allowed_origin)
|
||||
|
||||
if pattern ~= nil then
|
||||
if origin:match(pattern) then
|
||||
core.Debug("Test: " .. pattern .. ", Origin: " .. origin .. ", Match: yes")
|
||||
return origin
|
||||
else
|
||||
core.Debug("Test: " .. pattern .. ", Origin: " .. origin .. ", Match: no")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 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)
|
||||
-- allowed_headers: Comma-delimited list of allowed headers. (e.g. X-Header1,X-Header2)
|
||||
function preflight_request_ver1(txn, allowed_methods, allowed_headers)
|
||||
core.Debug("CORS: preflight request received")
|
||||
txn.http:res_set_header("Access-Control-Allow-Methods", allowed_methods)
|
||||
txn.http:res_set_header("Access-Control-Allow-Headers", allowed_headers)
|
||||
txn.http:res_set_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)
|
||||
-- allowed_headers: Comma-delimited list of allowed headers. (e.g. X-Header1,X-Header2)
|
||||
function preflight_request_ver2(txn, origin, allowed_methods, allowed_origins, allowed_headers)
|
||||
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-Allow-Headers", allowed_headers)
|
||||
reply:add_header("Access-Control-Max-Age", 600)
|
||||
|
||||
local allowed_origin = M.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)
|
||||
|
||||
if allowed_origin ~= "*" then
|
||||
reply:add_header("Vary", "Accept-Encoding,Origin")
|
||||
end
|
||||
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)
|
||||
-- allowed_headers: Comma-delimited list of allowed headers. (e.g. X-Header1,X-Header2)
|
||||
function cors_request(txn, allowed_methods, allowed_origins, allowed_headers)
|
||||
local headers = txn.http:req_get_headers()
|
||||
local transaction_data = {}
|
||||
local origin = nil
|
||||
local allowed_origins = core.tokenize(allowed_origins, ",")
|
||||
|
||||
if headers["origin"] ~= nil and headers["origin"][0] ~= nil then
|
||||
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
|
||||
origin = headers["origin"][0]
|
||||
end
|
||||
|
||||
-- Bail if client did not send an Origin
|
||||
-- for example, it may be a regular OPTIONS request that is not a CORS preflight request
|
||||
if origin == nil or origin == '' then
|
||||
return
|
||||
end
|
||||
|
||||
transaction_data["origin"] = origin
|
||||
transaction_data["allowed_methods"] = allowed_methods
|
||||
transaction_data["allowed_origins"] = allowed_origins
|
||||
transaction_data["allowed_headers"] = allowed_headers
|
||||
|
||||
txn:set_priv(transaction_data)
|
||||
|
||||
local method = txn.sf:method()
|
||||
transaction_data["method"] = method
|
||||
|
||||
if method == "OPTIONS" and txn.reply ~= nil then
|
||||
preflight_request_ver2(txn, origin, allowed_methods, allowed_origins, allowed_headers)
|
||||
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()
|
||||
|
||||
if transaction_data == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local origin = transaction_data["origin"]
|
||||
local allowed_origins = transaction_data["allowed_origins"]
|
||||
local allowed_methods = transaction_data["allowed_methods"]
|
||||
local allowed_headers = transaction_data["allowed_headers"]
|
||||
local method = transaction_data["method"]
|
||||
|
||||
-- Bail if client did not send an Origin
|
||||
if origin == nil or origin == '' then
|
||||
return
|
||||
end
|
||||
|
||||
local allowed_origin = M.get_allowed_origin(origin, allowed_origins)
|
||||
|
||||
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, allowed_headers)
|
||||
end
|
||||
|
||||
core.Debug("CORS: " .. origin .. " allowed")
|
||||
txn.http:res_set_header("Access-Control-Allow-Origin", allowed_origin)
|
||||
|
||||
if allowed_origin ~= "*" then
|
||||
txn.http:res_add_header("Vary", "Accept-Encoding,Origin")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Register the actions with HAProxy
|
||||
core.register_action("cors", {"http-req"}, cors_request, 3)
|
||||
core.register_action("cors", {"http-res"}, cors_response, 0)
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user