Learn F5 Technologies, Get Answers & Share Community Solutions Join DevCentral

Filter by:
  • Solution
  • Technology
Answers

Adding CORS response headers

Hey all,

There are a number of other older (2013-era) threads about CORS headers, and I want to ask a specific question which has not been asked there:

Can I add a response header using HTTP::header insert within an HTTP_REQUEST?

In at least one CORS-related thread (https://devcentral.f5.com/questions/cors-irule-query), this is shown happening. However, in another thread (https://devcentral.f5.com/questions/access-control-allow-origin-on-f5) the answer includes code in the HTTP_REQUEST to set a variable and then in the HTTP_RESPONSE, a check is made on that variable and if it is set, the HTTP::header insert is used.

Basically, I want to include all my CORS-related code in one place. Currently, I am doing basic CORS (adding the ACAO header for GET/POST requests from my domain where the Origin request header is present) using my CDN (Akamai) and I have this iRule for CORS preflight responses:

when HTTP_REQUEST {
    if { ( [HTTP::method] equals "OPTIONS" ) and
         ( [HTTP::host] contains "mysite.com"] ) and
         ( [HTTP::header] exists "Access-Control-Request-Method") } {
            HTTP::respond 200 Access-Control-Allow-Origin "[HTTP::header Origin]" \
                              Access-Control-Allow-Methods "POST, GET, OPTIONS" \
                              Access-Control-Allow-Headers "[HTTP::header Access-Control-Request-Headers]" \
                              Access-Control-Max-Age "86400"
            return
    }
}

However, for simplification, I want to put all the CORS stuff (basic and preflight) in the iRule. So my question is, will this work:

when HTTP_REQUEST {
    # CORS preflight OPTIONS requests
    if { ( [HTTP::method] equals "OPTIONS" ) and
         ( [HTTP::host] contains "mysite.com"] ) and
         ( [HTTP::header] exists "Access-Control-Request-Method") } {
            HTTP::respond 200 Access-Control-Allow-Origin "[HTTP::header Origin]" \
                              Access-Control-Allow-Methods "POST, GET, OPTIONS" \
                              Access-Control-Allow-Headers "[HTTP::header Access-Control-Request-Headers]" \
                              Access-Control-Max-Age "86400"
            return
    }
    # CORS GET/POST requests
    if { ( [HTTP::method] equals "GET" or [HTTP::method] equals "POST") and
         ( [HTTP::host] contains "mysite.com"] ) and
         ( [HTTP::header] exists "Origin") } {
            HTTP::header insert Access-Control-Allow-Origin "[HTTP::header Origin]"
    }
}

or do I need this:

when HTTP_REQUEST {
    # CORS preflight OPTIONS requests
    if { ( [HTTP::method] equals "OPTIONS" ) and
         ( [HTTP::host] contains "mysite.com"] ) and
         ( [HTTP::header] exists "Access-Control-Request-Method") } {
            HTTP::respond 200 Access-Control-Allow-Origin "[HTTP::header Origin]" \
                              Access-Control-Allow-Methods "POST, GET, OPTIONS" \
                              Access-Control-Allow-Headers "[HTTP::header Access-Control-Request-Headers]" \
                              Access-Control-Max-Age "86400"
            return
    }
    # CORS GET/POST requests
    if { ( [HTTP::host] contains "mysite.com"] ) and
         ( [HTTP::header] exists "Origin") } {
            set cors_origin [HTTP::header Origin]
    }
}
when HTTP_RESPONSE {
    # CORS GET/POST response - check variable set in request
    if { [info exists cors_origin] } {
            HTTP::header insert Access-Control-Allow-Origin $cors_origin
    }
}

Does this make sense, or am I getting too complex?

0
Rate this Question
Comments on this Question
Comment made 08-Oct-2015 by Rory Hewitt F5 107
Note: I have added our eventual solution at the bottom, in case anyone else has the same issues in the future.
0

Answers to this Question

placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

The solution depends on where you want the traffic to go. If you want it to go to the client, then it's in the response event. To the server then it's in the request event. The HTTP::respond command is a little unique here because it forces a preemptive response to the client from a request event - in other words the server never sees the request. The iTule preemptively sends a response.

If you want to send additional data to the client based on something they said in the request, then the best approach is to set a variable in the request and look for that variable in the response event.

0
Comments on this Answer
Comment made 10-Jul-2015 by Rory Hewitt F5 107
Hey Kevin, that's what I thought. So my second example would be the way to go then. Thanks!
0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

To anyone who comes in afterwards and wants to find a 'final' solution, here's what we ended up with (which functions perfectly, at least for us):

when HTTP_REQUEST priority 200 {
    unset cors_origin -nocomplain
    if { [HTTP::header Origin] ends_with ".example.com" } {
        if { ( [HTTP::method] equals "OPTIONS" ) and ( [HTTP::header exists "Access-Control-Request-Method"] ) } {
            # CORS preflight request - return response immediately
            HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
                              "Access-Control-Allow-Methods" "POST, GET, OPTIONS" \
                              "Access-Control-Allow-Headers" [HTTP::header "Access-Control-Request-Headers"] \
                              "Access-Control-Max-Age" "86400"
        } else {
            # CORS GET/POST requests - set cors_origin variable
            set cors_origin [HTTP::header "Origin"]
        }
    }
    ...
    ...
    ...
    other irules
    ...
    ...
    ...
}
when HTTP_RESPONSE {
    # CORS GET/POST response - check cors_origin variable set in request
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        HTTP::header insert "Access-Control-Allow-Credentials" "true"
        HTTP::header insert "Vary" "Origin"
    }
}

If you have any comments about this, please do so. And, of course, feel free to use it yourself.

1
Comments on this Answer
Comment made 17-Apr-2018 by Thomas Schaefer 66

Thanks for the code. One point is this code fails: unset cors_origin -nocomplain

After reading the Tcl info on unset, this works: unset -nocomplain cors_origin

Tom

0
Comment made 17-Apr-2018 by Rory Hewitt F5 107

You're right. Apologies - I thought I had updated this code to fix that particular buglet. If I can, I'll update it now.

1
Comment made 17-Apr-2018 by Rory Hewitt F5 107

Check out https://devcentral.f5.com/codeshare/cors-implementation, which contains the 'correct' implementation.

1
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Hi,

We have a requirement to restrict CORS requests to domains owned by our company, or by subsidaries/partners. This rule works well for us;-

when HTTP_REQUEST {

    set debug 0
    set Origin ""

    # Can be used in prod for debugging on a per-connection basis, using a browser add-on like Firebug to add the headers
    if {[HTTP::header exists "X-tls-debug"]} {
        set debug 1
    }

    # log prefix for connection/request tracking. All debug logs will start with [xxxx.yyyy] where xxxx is the connection and yyyy is the request id
    if {$debug} {   
        # per request identifier
        set prefix "\[[TCP::client_port].[expr {int (rand() * 100000)}]\] " 
    }

    if {[HTTP::header exists Origin]} {
        # This is a cross-origin request
        if {$debug} {log local0. "${prefix} Origin:[HTTP::header Origin]"}
        switch -glob -- [HTTP::header Origin] {
            "*company.com" -
            "*company.com.au" -
            "*partner.com" -
            "*partner2.com.au" {
                # This is  a valid Origin
                switch -- [HTTP::method] {
                    "OPTIONS" {
                        if {$debug} {log local0. "${prefix} Responding to Preflight request"}
                        HTTP::respond 200 noserver  Allow "GET,HEAD,POST,OPTIONS" \
                                                    Access-Control-Allow-Origin "[HTTP::header Origin]" \
                                                    Access-Control-Allow-Methods "GET,POST" \
                                                    Access-Control-Max-Age "86400"
                        return
                    }
                    "GET" -
                    "POST" {
                        if {$debug} {log local0. "${prefix} Origin:[HTTP::header Origin]"}
                        # Set variable to add headers to response
                        set Origin [HTTP::header Origin]
                    }
                }
            }
            default {
                # This is a cross-origin request,  however it's not from the correct domain
                if {[HTTP::method] eq "OPTIONS"} {
                    if {$debug} {log local0. "${prefix} Responding to OPTIONS method"}
                    HTTP::respond 200 noserver Allow "GET,POST,HEAD,OPTIONS"
                    return
                }
                # So what if it's not from correct domain,  but is GET/POST - still gonna send a response,  just not add in any CORS headers,  which will make response fail at browser
            }
        }
    }   
}
when HTTP_RESPONSE {

    if {$Origin ne ""} {
        # This response needs CORS headers inserted     
       HTTP::header insert "Access-Control-Allow-Origin" $Origin
       HTTP::header insert "Access-Control-Allow-Methods" "GET,POST"
       HTTP::header insert "Access-Control-Max-Age" "86400"
       HTTP::header insert "Allow" "GET,HEAD,POST,OPTIONS"
    }

    # Don't forget this - if a request is CORS-enabled,  you ALWAYS need to instruct a cache to Vary a response based on Origin,
    # even if this particular request was not a valid cross-origin request. 
    HTTP::header insert "Vary" "Origin"
}
0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

I am now having a separate problem related to a new version of sample code above. Here's the problem fragment:

if { ( [HTTP::method] equals "OPTIONS" ) and ( [HTTP::host] contains "example.com"] ) and ( [HTTP::header] exists "Access-Control-Request-Method") } {
    HTTP::respond 200 "Access-Control-Allow-Origin" "[HTTP::header Origin]" "Access-Control-Allow-Methods" "POST, GET, OPTIONS" "Access-Control-Allow-Headers" "[HTTP::header Access-Control-Request-Headers]" "Access-Control-Max-Age" "86400"        
} elseif { ( [HTTP::host] contains "example.com"] ) and ( [HTTP::header] exists "Origin") } {
    # CORS GET/POST requests - set cors_origin variable
    set cors_origin [HTTP::header Origin]
}

As you can see, each of the CORS response header names in the second line is enclosed in double-quotes, so the iRule treats them as strings.

However, when I try to deploy the iRule that contains this fragment, it fails, and I'm not sure why.

Could the problem be this bit:

"Access-Control-Allow-Headers" "[HTTP::header Access-Control-Request-Headers]"

Could Tcl think that Access-Control-Request-Headers is not a string? If so, what's the solution? Can I have a string within a string?

0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Just try removing the quotes from all the header names as you did in your original post.

0
Comments on this Answer
Comment made 07-Sep-2015 by Rory Hewitt F5 107
I tried that - in fact my original version was a semi-direct copy of your answer in https://devcentral.f5.com/questions/access-control-allow-origin-on-f5. However, there still seem to be some problems. Are you saying that this should work: if { ( [HTTP::method] equals "OPTIONS" ) and ( [HTTP::host] contains "fds.com"] ) and ( [HTTP::header] exists "Access-Control-Request-Method") } { HTTP::respond 200 Access-Control-Allow-Origin [HTTP::header Origin] \ Access-Control-Allow-Methods "POST, GET, OPTIONS" \ Access-Control-Allow-Headers [HTTP::header Access-Control-Request-Headers] \ Access-Control-Max-Age "86400" } elseif { ( [HTTP::host] contains "fds.com"] ) and ( [HTTP::header] exists "Origin") } { # CORS GET/POST requests - set cors_origin variable set cors_origin [HTTP::header "Origin"] } Because it doesn't seem to be doing so for me.
0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Are you sure that there is a request header Access-Control-Request-Headers in the request? Also when you say it's not working is the F5 refusing to save the iRule, or are you getting a TCL error when you execute, or is the client browser not liking the response to the OPTIONS request?

Also as a general comment be careful of intermediate downstream caches for GET requests. If the response is cacheable, and made over HTTP, then you either need to make it not cacheable OR insert "Vary:Origin".

0
Comments on this Answer
Comment made 07-Sep-2015 by Rory Hewitt F5 107
I'm having problems even deploying the iRule. To be honest, although I know Tcl from a long time ago, I don't know F5 or how the iRules are combined to make a deployable XML file (or whatever it is). As far as I know, the specific iRule (Tcl file) is being saved, but when multiple iRules are being combined (with assorted other TXT files), something is failing, and the final XML file is missing all the Tcl files. Given that my change is in the first Tcl file and given that it's the only change form the last (working) version, I'm assuming that there is a syntax bug in there somewhere... As far as adding the Vary:Origin header, we've removed that from the caching we're doing (in Akamai). I'm sadly well aware of the concerns about the Vary header...
0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

OK I see what you've done - a few syntax issues. Not sure what you are referring to when you talk about XML/TXT files, but I recommend that you get yourself a VE lab license and run a VE instance on your computer to at least syntax check a rule before deploying it.

Note that you must ensure that you do not execute any other commands after your HTTP::respond, and especially if you are combining iRules, it's best practice to add an explicit "return" afterwards.

if { [HTTP::method] eq "OPTIONS" && [HTTP::host] contains "example.com"  && [HTTP::header exists "Access-Control-Request-Method"] } {
        HTTP::respond 200 Access-Control-Allow-Origin "[HTTP::header Origin]" Access-Control-Allow-Methods "POST, GET, OPTIONS" Access-Control-Allow-Headers "[HTTP::header Access-Control-Request-Headers]" Access-Control-Max-Age "86400"
        return
    } elseif { [HTTP::host] contains "example.com"  &&  [HTTP::header exists "Origin"] } {
        # CORS GET/POST requests - set cors_origin variable
        set cors_origin [HTTP::header Origin]
}
0
Comments on this Answer
Comment made 08-Sep-2015 by Rory Hewitt F5 107
Hmmm. When I compare yours with mine, I see that you're using 'eq' and '&&' rather than 'equals' and 'and', but those function exactly the same, don't they? Also I can see that you're not using all the additional parentheses around each sub-clause, but that SEEMS like a stylistic thing, right? The only DIFFERENCE is that you're not putting the header names in quotes, but you ARE including the entire value for each header in double-quotes. but again, shouldn't my version also work? Isn't this: HTTP::respond 200 Access-Control-Allow-Origin "[HTTP::header Origin]" the same as this: HTTP::respond 200 Access-Control-Allow-Origin [HTTP::header "Origin"] I REALLY appreciate your help, but the team that actually deploys these iRules to our environments won't accept a new version from me, and I guess I'm still not clear on what's actually WRONG with my version - where exactly is my error? The XML file I'm talking about is the BigIpConfiguration file.
0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

To be honest I hadn't even realised that 'equals' or 'and' were also valid!! Learn something new every day :-)

Yes you are correct - even this (no quotes at all) will work;-

HTTP::respond 200 Access-Control-Allow-Origin [HTTP::header Origin] 

Where you actually went wrong though was

[HTTP::header] exists "Origin"

instead of

[HTTP::header exists "Origin"]
0
Comments on this Answer
Comment made 08-Sep-2015 by IheartF5 2377
and will just say again you need a 'dev' environment.....
0
Comment made 08-Sep-2015 by Rory Hewitt F5 107
Aaaaaaaaaggggggghhhhhhhhh. That's where I went wrong. Sonofa...! If I were doing this regularly, I would do that. In this case, I'm the CORS expert, not the iRules expert, so I put my (obviously rusty) Tcl knowledge plus what little I know of iRules to attempt to implement an iRule solution. I then sent this code to our iRules guy, who coded it...
0