Skip to content

Commit e62d3f1

Browse files
authored
Merge pull request #15 from haproxytech/preflight_reply
Fixes #6: Preflight requests can be terminated and returned immediately
2 parents 83cdc7c + cd1ffb4 commit e62d3f1

File tree

5 files changed

+219
-80
lines changed

5 files changed

+219
-80
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ global
2929
lua-load /path/to/cors.lua
3030
```
3131

32-
In your `frontend` or `listen` section, capture the client's *Origin* request header by adding `http-request lua.cors`:
32+
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.
3333

3434
```
35-
http-request lua.cors
35+
http-request lua.cors "GET,PUT,POST" "example.com,localhost,localhost:8080"
3636
```
3737

38-
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.
38+
Within the same section, invoke the `http-response lua.cors` action to attach CORS headers to responses from backend servers.
3939

4040
```
41-
http-response lua.cors "GET,PUT,POST" "example.com,localhost,localhost:8080"
41+
http-response lua.cors
4242
```
4343

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

4646
```
47-
http-response lua.cors "GET,PUT,POST" "*"
47+
http-request lua.cors "GET,PUT,POST" "*"
4848
```

example/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ services:
1010
- "name=server1"
1111

1212
haproxy:
13-
image: haproxytech/haproxy-ubuntu:2.0
13+
image: haproxytech/haproxy-ubuntu:2.2
1414
volumes:
1515
- "./haproxy/haproxy.cfg:/etc/haproxy/haproxy.cfg"
1616
- "./haproxy/cors.lua:/etc/haproxy/cors.lua"

example/haproxy/cors.lua

Lines changed: 108 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,59 +20,131 @@ function contains(items, test_str)
2020
return false
2121
end
2222

23-
-- When invoked during a request, captures the Origin header if present
24-
-- and stores it in a private variable.
25-
function cors_request(txn)
26-
local headers = txn.http:req_get_headers()
27-
local origin = headers["origin"]
28-
23+
-- If the given origin is found within the allowed_origins string, it is returned. Otherwise, nil is returned.
24+
-- origin: The value from the 'origin' request header
25+
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
26+
function get_allowed_origin(origin, allowed_origins)
2927
if origin ~= nil then
30-
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
31-
txn:set_priv(headers["origin"][0])
28+
local allowed_origins = core.tokenize(allowed_origins, ",")
29+
30+
-- Strip whitespace
31+
for index, value in ipairs(allowed_origins) do
32+
allowed_origins[index] = value:gsub("%s+", "")
33+
end
34+
35+
if contains(allowed_origins, "*") then
36+
return "*"
37+
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
38+
return origin
39+
end
3240
end
41+
42+
return nil
3343
end
3444

35-
-- When invoked during a response, sets CORS headers so that the browser
36-
-- can read the response from permitted domains.
37-
-- txn: The current transaction object that gives access to response properties.
45+
-- Adds headers for CORS preflight request and then attaches them to the response
46+
-- after it comes back from the server. This works with versions of HAProxy prior to 2.2.
47+
-- The downside is that the OPTIONS request must be sent to the backend server first and can't
48+
-- be intercepted and returned immediately.
49+
-- txn: The current transaction object that gives access to response properties
50+
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
51+
function preflight_request_ver1(txn, allowed_methods)
52+
core.Debug("CORS: preflight request received")
53+
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
54+
txn.http:res_add_header("Access-Control-Max-Age", 600)
55+
core.Debug("CORS: attaching allowed methods to response")
56+
end
57+
58+
-- Add headers for CORS preflight request and then returns a 204 response.
59+
-- The 'reply' function used here is available in HAProxy 2.2+. It allows HAProxy to return
60+
-- a reply without contacting the server.
61+
-- txn: The current transaction object that gives access to response properties
62+
-- origin: The value from the 'origin' request header
63+
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
64+
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
65+
function preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
66+
core.Debug("CORS: preflight request received")
67+
68+
local reply = txn:reply()
69+
reply:set_status(204, "No Content")
70+
reply:add_header("Content-Type", "text/html")
71+
reply:add_header("Access-Control-Allow-Methods", allowed_methods)
72+
reply:add_header("Access-Control-Max-Age", 600)
73+
74+
local allowed_origin = get_allowed_origin(origin, allowed_origins)
75+
76+
if allowed_origin == nil then
77+
core.Debug("CORS: " .. origin .. " not allowed")
78+
else
79+
core.Debug("CORS: " .. origin .. " allowed")
80+
reply:add_header("Access-Control-Allow-Origin", allowed_origin)
81+
end
82+
83+
core.Debug("CORS: Returning reply to preflight request")
84+
txn:done(reply)
85+
end
86+
87+
-- When invoked during a request, captures the origin header if present and stores it in a private variable.
88+
-- If the request is OPTIONS and it is a supported version of HAProxy, returns a preflight request reply.
89+
-- Otherwise, the preflight request header is added to the response after it has returned from the server.
90+
-- txn: The current transaction object that gives access to response properties
3891
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
3992
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
40-
function cors_response(txn, allowed_methods, allowed_origins)
93+
function cors_request(txn, allowed_methods, allowed_origins)
94+
local headers = txn.http:req_get_headers()
95+
local origin = headers["origin"][0]
96+
97+
local transaction_data = {}
98+
99+
if origin ~= nil then
100+
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
101+
transaction_data["origin"] = origin
102+
end
103+
104+
transaction_data["allowed_methods"] = allowed_methods
105+
transaction_data["allowed_origins"] = allowed_origins
106+
107+
txn:set_priv(transaction_data)
108+
41109
local method = txn.sf:method()
42-
local origin = txn:get_priv()
43-
44-
-- add headers for CORS preflight request
45-
if method == "OPTIONS" then
46-
core.Debug("CORS: preflight request OPTIONS")
47-
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
48-
txn.http:res_set_header("Allow", allowed_methods)
49-
txn.http:res_add_header("Access-Control-Max-Age", 600)
110+
transaction_data["method"] = method
111+
112+
if method == "OPTIONS" and txn.reply ~= nil then
113+
preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
50114
end
115+
end
116+
117+
-- When invoked during a response, sets CORS headers so that the browser can read the response from permitted domains.
118+
-- txn: The current transaction object that gives access to response properties.
119+
function cors_response(txn)
120+
local transaction_data = txn:get_priv()
121+
local origin = transaction_data["origin"]
122+
local allowed_origins = transaction_data["allowed_origins"]
123+
local allowed_methods = transaction_data["allowed_methods"]
124+
local method = transaction_data["method"]
125+
126+
-- Always vary on the Origin
127+
txn.http:res_add_header("Vary", "Accept-Encoding,Origin")
51128

52129
-- Bail if client did not send an Origin
53130
if origin == nil or origin == '' then
54131
return
55132
end
56133

57-
local allowed_origins = core.tokenize(allowed_origins, ",")
58-
59-
-- Strip whitespace
60-
for index, value in ipairs(allowed_origins) do
61-
allowed_origins[index] = value:gsub("%s+", "")
62-
end
134+
local allowed_origin = get_allowed_origin(origin, allowed_origins)
63135

64-
if contains(allowed_origins, "*") then
65-
core.Debug("CORS: " .. "* allowed")
66-
txn.http:res_add_header("Access-Control-Allow-Origin", "*")
67-
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
68-
core.Debug("CORS: " .. origin .. " allowed")
69-
txn.http:res_add_header("Access-Control-Allow-Origin", origin)
70-
txn.http:res_add_header("Vary", "Accept-Encoding,Origin")
71-
else
136+
if allowed_origin == nil then
72137
core.Debug("CORS: " .. origin .. " not allowed")
138+
else
139+
if method == "OPTIONS" and txn.reply == nil then
140+
preflight_request_ver1(txn, allowed_methods)
141+
end
142+
143+
core.Debug("CORS: " .. origin .. " allowed")
144+
txn.http:res_add_header("Access-Control-Allow-Origin", allowed_origin)
73145
end
74146
end
75147

76148
-- Register the actions with HAProxy
77-
core.register_action("cors", {"http-req"}, cors_request, 0)
78-
core.register_action("cors", {"http-res"}, cors_response, 2)
149+
core.register_action("cors", {"http-req"}, cors_request, 2)
150+
core.register_action("cors", {"http-res"}, cors_response, 0)

example/haproxy/haproxy.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ listen api
1919
bind :8080
2020

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

2424
# Invoke the CORS service on the response to add CORS headers
25-
http-response lua.cors "GET,PUT,POST" "localhost"
25+
http-response lua.cors
2626
server s1 server1:80 check

lib/cors.lua

Lines changed: 103 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,35 +20,108 @@ function contains(items, test_str)
2020
return false
2121
end
2222

23-
-- When invoked during a request, captures the Origin header if present
24-
-- and stores it in a private variable.
25-
function cors_request(txn)
26-
local headers = txn.http:req_get_headers()
27-
local origin = headers["origin"]
28-
23+
-- If the given origin is found within the allowed_origins string, it is returned. Otherwise, nil is returned.
24+
-- origin: The value from the 'origin' request header
25+
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
26+
function get_allowed_origin(origin, allowed_origins)
2927
if origin ~= nil then
30-
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
31-
txn:set_priv(headers["origin"][0])
28+
local allowed_origins = core.tokenize(allowed_origins, ",")
29+
30+
-- Strip whitespace
31+
for index, value in ipairs(allowed_origins) do
32+
allowed_origins[index] = value:gsub("%s+", "")
33+
end
34+
35+
if contains(allowed_origins, "*") then
36+
return "*"
37+
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
38+
return origin
39+
end
3240
end
41+
42+
return nil
3343
end
3444

35-
-- Add headers for CORS preflight request
36-
function preflight_request(txn, method, allowed_methods)
37-
if method == "OPTIONS" then
38-
core.Debug("CORS: preflight request OPTIONS")
39-
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
40-
txn.http:res_add_header("Access-Control-Max-Age", 600)
45+
-- Adds headers for CORS preflight request and then attaches them to the response
46+
-- after it comes back from the server. This works with versions of HAProxy prior to 2.2.
47+
-- The downside is that the OPTIONS request must be sent to the backend server first and can't
48+
-- be intercepted and returned immediately.
49+
-- txn: The current transaction object that gives access to response properties
50+
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
51+
function preflight_request_ver1(txn, allowed_methods)
52+
core.Debug("CORS: preflight request received")
53+
txn.http:res_add_header("Access-Control-Allow-Methods", allowed_methods)
54+
txn.http:res_add_header("Access-Control-Max-Age", 600)
55+
core.Debug("CORS: attaching allowed methods to response")
56+
end
57+
58+
-- Add headers for CORS preflight request and then returns a 204 response.
59+
-- The 'reply' function used here is available in HAProxy 2.2+. It allows HAProxy to return
60+
-- a reply without contacting the server.
61+
-- txn: The current transaction object that gives access to response properties
62+
-- origin: The value from the 'origin' request header
63+
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
64+
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
65+
function preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
66+
core.Debug("CORS: preflight request received")
67+
68+
local reply = txn:reply()
69+
reply:set_status(204, "No Content")
70+
reply:add_header("Content-Type", "text/html")
71+
reply:add_header("Access-Control-Allow-Methods", allowed_methods)
72+
reply:add_header("Access-Control-Max-Age", 600)
73+
74+
local allowed_origin = get_allowed_origin(origin, allowed_origins)
75+
76+
if allowed_origin == nil then
77+
core.Debug("CORS: " .. origin .. " not allowed")
78+
else
79+
core.Debug("CORS: " .. origin .. " allowed")
80+
reply:add_header("Access-Control-Allow-Origin", allowed_origin)
4181
end
82+
83+
core.Debug("CORS: Returning reply to preflight request")
84+
txn:done(reply)
4285
end
4386

44-
-- When invoked during a response, sets CORS headers so that the browser
45-
-- can read the response from permitted domains.
46-
-- txn: The current transaction object that gives access to response properties.
87+
-- When invoked during a request, captures the origin header if present and stores it in a private variable.
88+
-- If the request is OPTIONS and it is a supported version of HAProxy, returns a preflight request reply.
89+
-- Otherwise, the preflight request header is added to the response after it has returned from the server.
90+
-- txn: The current transaction object that gives access to response properties
4791
-- allowed_methods: Comma-delimited list of allowed HTTP methods. (e.g. GET,POST,PUT,DELETE)
4892
-- allowed_origins: Comma-delimited list of allowed origins. (e.g. localhost,localhost:8080,test.com)
49-
function cors_response(txn, allowed_methods, allowed_origins)
93+
function cors_request(txn, allowed_methods, allowed_origins)
94+
local headers = txn.http:req_get_headers()
95+
local origin = headers["origin"][0]
96+
97+
local transaction_data = {}
98+
99+
if origin ~= nil then
100+
core.Debug("CORS: Got 'Origin' header: " .. headers["origin"][0])
101+
transaction_data["origin"] = origin
102+
end
103+
104+
transaction_data["allowed_methods"] = allowed_methods
105+
transaction_data["allowed_origins"] = allowed_origins
106+
107+
txn:set_priv(transaction_data)
108+
50109
local method = txn.sf:method()
51-
local origin = txn:get_priv()
110+
transaction_data["method"] = method
111+
112+
if method == "OPTIONS" and txn.reply ~= nil then
113+
preflight_request_ver2(txn, origin, allowed_methods, allowed_origins)
114+
end
115+
end
116+
117+
-- When invoked during a response, sets CORS headers so that the browser can read the response from permitted domains.
118+
-- txn: The current transaction object that gives access to response properties.
119+
function cors_response(txn)
120+
local transaction_data = txn:get_priv()
121+
local origin = transaction_data["origin"]
122+
local allowed_origins = transaction_data["allowed_origins"]
123+
local allowed_methods = transaction_data["allowed_methods"]
124+
local method = transaction_data["method"]
52125

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

61-
local allowed_origins = core.tokenize(allowed_origins, ",")
62-
63-
-- Strip whitespace
64-
for index, value in ipairs(allowed_origins) do
65-
allowed_origins[index] = value:gsub("%s+", "")
66-
end
134+
local allowed_origin = get_allowed_origin(origin, allowed_origins)
67135

68-
if contains(allowed_origins, "*") then
69-
core.Debug("CORS: " .. "* allowed")
70-
txn.http:res_add_header("Access-Control-Allow-Origin", "*")
71-
preflight_request(txn, method, allowed_methods)
72-
elseif contains(allowed_origins, origin:match("//([^/]+)")) then
73-
core.Debug("CORS: " .. origin .. " allowed")
74-
txn.http:res_add_header("Access-Control-Allow-Origin", origin)
75-
preflight_request(txn, method, allowed_methods)
76-
else
136+
if allowed_origin == nil then
77137
core.Debug("CORS: " .. origin .. " not allowed")
138+
else
139+
if method == "OPTIONS" and txn.reply == nil then
140+
preflight_request_ver1(txn, allowed_methods)
141+
end
142+
143+
core.Debug("CORS: " .. origin .. " allowed")
144+
txn.http:res_add_header("Access-Control-Allow-Origin", allowed_origin)
78145
end
79146
end
80147

81148
-- Register the actions with HAProxy
82-
core.register_action("cors", {"http-req"}, cors_request, 0)
83-
core.register_action("cors", {"http-res"}, cors_response, 2)
149+
core.register_action("cors", {"http-req"}, cors_request, 2)
150+
core.register_action("cors", {"http-res"}, cors_response, 0)

0 commit comments

Comments
 (0)