Forum Discussion

Rory_Hewitt_F5_'s avatar
Jul 10, 2015
Solved

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?

 

  • 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.

     

13 Replies

  • 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.

     

  • 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"
    }
    

     

  • 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?

     

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

     

    • Rory_Hewitt_F5_'s avatar
      Rory_Hewitt_F5_
      Icon for Cirrus rankCirrus
      I tried that - in fact my original version was a semi-direct copy of your answer in https://devcentral.f5.com/s/feed/0D51T00006i7X7cSAE. 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.
  • 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".

     

    • Rory_Hewitt_F5_'s avatar
      Rory_Hewitt_F5_
      Icon for Cirrus rankCirrus
      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...
  • 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]
    }
    

     

    • Rory_Hewitt_F5_'s avatar
      Rory_Hewitt_F5_
      Icon for Cirrus rankCirrus
      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.
  • 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.

     

  • Note: I have added our eventual solution at the bottom, in case anyone else has the same issues in the future.