HAProxy CORS OPTIONS header intercept setup
Asked Answered
A

4

15

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!

Assuntaassur answered 23/9, 2015 at 21:9 Comment(1)
I am getting [2016-04-19 12:49:54.625] uaa - 5213 [http-bio-8080-exec-5] .... DEBUG --- CorsFilter: CORS processing completed for: URI: /login.do; Scheme: https; Host: uaa.testinception25.io; Port: 443; Origin: skyfallui-testinception25.com; Method: POST Status:403 Help me the haproxy setting for thisFloruit
G
8

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.

Garceau answered 6/9, 2017 at 23:2 Comment(1)
This is an awesome solution! Any way to make it work with http2 enabled?Hamulus
O
6

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)
Ounce answered 4/7, 2016 at 21:55 Comment(0)
A
4

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.

Antipode answered 22/9, 2017 at 18:1 Comment(0)
H
0

To intercept OPTIONS like in my case, I want to add Access-Control-Max-Age header to response. I modify my haproxy.cfg and add this line to the backend block.

backend api
  balance roundrobin
  http-response set-header Access-Control-Max-Age 600 if METH_OPTIONS

HAProxy version: 2.2.6

Reference Doc: https://cbonte.github.io/haproxy-dconv/2.2/configuration.html#http-response%20set-header

Hartsell answered 9/7, 2021 at 5:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.