Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HAProxy CORS OPTIONS header intercept setup

With my NGinx setup I was able to intercept OPTIONS requests from ajax preflight and respond with the correct CORS headers and a 200 response so the request could continue onwards. I am attempting to consolidate my frontend proxies into HAProxy and am having some issues getting this piece of the puzzle working.

My particular issue is that while I am able to add the proper CORS options when there is a server able to respond properly to an OPTIONS request, a few of the backends cannot handle/respond with a 405 error when the preflight request is issued. My haproxy.cfg included the following lines for adding the headers:

capture request header origin len 128
http-response add-header Access-Control-Allow-Origin %[capture.req.hdr(0)] if { capture.req.hdr(0) -m found }
rspadd Access-Control-Allow-Credentials:\ true if { capture.req.hdr(0) -m found }
rspadd Access-Control-Allow-Headers:\ Origin,\ X-Requested-With,\ Content-Type,\ Origin,\ User-Agent,\ If-Modified-Since,\ Cache-Control,\ Accept if { capture.req.hdr(0) -m found }
rspadd Access-Control-Allow-Methods:\ GET,\ POST,\ PUT,\ DELETE,\ OPTIONS if { capture.req.hdr(0) -m found }
rspadd Access-Control-Max-Age:\ 1728000 if { capture.req.hdr(0) -m found }

The solution given in:

How to send a response with HAProxy without passing the request to web servers works when you set all of the correct headers from a client's request, but is not dynamic which is not an ideal solution.

Any help would be appreciated!

like image 766
Brian Avatar asked Sep 23 '15 21:09

Brian


3 Answers

Based on the great answer from anine.io I came up with the following solution which allows to define a list of allowed origins and it also adds the missing Acccess-Control-Allow-Origin Header for all HTTP requests. The answer from anine.io only showed the CORS preflight, but didn't consider the normal requests.

In haproxy.cfg load the cors.lua file (adapt the path if necessary) in the global section

global
    lua-load /usr/local/etc/haproxy/cors.lua

Add the CORS configuration to your frontend definition

frontend http-in
    # CORS configuration
    # capture origin HTTP header
    capture request header origin len 128
    # add Access-Control-Allow-Origin HTTP header to response if origin matches the list of allowed URLs
    http-response add-header Access-Control-Allow-Origin %[capture.req.hdr(0)] if !METH_OPTIONS { capture.req.hdr(0) -m reg -f /usr/local/etc/haproxy/cors-origins.lst }
    # if a preflight request is made, use CORS preflight backend
    http-request use-service lua.cors-response if METH_OPTIONS { capture.req.hdr(0) -m reg -f /usr/local/etc/haproxy/cors-origins.lst }

Create a file called cors.lua and store it under the path you specified above. The file contains the CORS preflight and if there's no good reason, don't restrict Methods or Headers because you would have to include any restrictions regarding methods or headers in the ACLs defined in the CORS configuration in haproxy.conf. Note: Currently Browsers do not support wildcard * for the Access-Control-Allow-Methods header. The cors.lua file should contain the following content

core.register_service("cors-response", "http", function(applet)
    applet:set_status(200)
    applet:add_header("Content-Length", "0")
    applet:add_header("Access-Control-Allow-Origin", applet.headers["origin"][0])
    applet:add_header("Access-Control-Allow-Credentials", "true")
    applet:add_header("Access-Control-Allow-Headers", "*")
    applet:add_header("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS")
    applet:add_header("Access-Control-Max-Age", "1728000")
    applet:start_response()
end)

Create a file called cors-origins.lst and store it under the path you specified above in the CORS configuration. The file should contain regular expressions (or just simple strings). If the client sends an Origin header, it will be validated against these regular expressions and only if they match, the CORS Preflight from cors.lua will be returned (for HTTP OPTIONS requests) or the Access-Control-Allow-Origin with the value of the origin header of the client request will be added to the response. An example of the content of cors-origins.lst could be

example.com
localhost.*
.*\.mydomain\.com:[8080|8443]

Test the configuration with http://test-cors.org/. For GET requests there should be no CORS Preflight. For requests other than GET, a CORS Preflight request should be done by the client first (e.g. an HTTP OPTIONS call) to check if the intended method, headers and authorization is allowed.

See HTTP access control (CORS) for further details regarding CORS.

like image 143
Florian Feldhaus Avatar answered Nov 15 '22 03:11

Florian Feldhaus


You can use Lua, but you need to make sure HAproxy is built with USE_LUA, by checking haproxy -vv.

This is an example configuration, I haven't tried it myself, but it will give you an idea of what you can do:

# haproxy.cfg

global
    lua-load cors.lua

frontend foo
    ...
    http-request use-service lua.cors-response if METH_OPTIONS { req.hdr(origin) -m found } { ... }

# cors.lua
core.register_service("cors-response", "http", function(applet)
    applet:set_status(200)
    applet:add_header("Content-Length", "0")
    applet:add_header("Access-Control-Allow-Origin", applet.headers["origin"][0])
    applet:add_header("Access-Control-Allow-Credentials", "true")
    applet:add_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Origin, User-Agent, If-Modified-Since, Cache-Control, Accept")
    applet:add_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
    applet:add_header("Access-Control-Max-Age", "1728000")
    applet:start_response()
end)
like image 20
anine.io Avatar answered Nov 15 '22 02:11

anine.io


I want to throw my own answer in the ring here. We've gone through quite some pain to arrive at a working setup. The other answers to this question here were very helpful, but didn't yield a fully working configuration for us.

We wanted to allow any origin. If you need whitelisted origins, see the answer from @Florian Feldhaus for a useful regex trick. Instead of using a whitelist, we echo back the location header:

http-request set-header Access-Control-Allow-Origin %[capture.req.hdr(0)] if { capture.req.hdr(0) -m found }

We also needed to explicitly set Access-Control-Allow-Headers and Access-Control-Expose-Headers. Browser support for wildcards in theses headers is not quite there yet.

So here's what our configuration does:

  1. handles preflight requests using this cors.lua script
  2. handles normal request using http-response set-header to add Access-Control-Allow-* headers
  3. adjust tune.maxrewrite to fit our CORS headers (which are > 1 KB)

The steps for 1) and 2) are explained in the other answers here, but step 3) took us a long time to figure out. I have documented the full config and the journey that took us there in this blog post. The post contains links to the gist on github.

like image 3
Johannes Rudolph Avatar answered Nov 15 '22 02:11

Johannes Rudolph