While traveling and talking about iRules, I come across a wide variety of topics that people are looking to address. From security offload to custom routing to just about anything you can think of. This week, while out and about at User Groups, I was presented with a problem to solve that was interesting. I ended up white board solving the problem in front of the group, and wanted to share it here as well, as I'm confident others may run into similar issues in one way or another.

The particular user was in the middle of migrating to a new URL format for their application, pushing out new domains for users to make use of. Previously they had been running off of a single, main domain and using individual URIs to serve different portions of their application to users. They were now hosting individual domains for each app section, and were pushing users to make use of the new domains instead of the old domain + URI combination. As is often the case, however, old habits die hard. It seems that their users were used to doing things the old way, had shortcuts created, or just weren't listening.

On top of that, the back end system still requires the old domain + URI format, which means if they want to push the new format on users, they need to find a way to rewrite things mid-stream so the servers stay happy. Given all that, they started groping around for a way to force the behavior they wanted, and that search quickly turned to their F5 devices and iRules. Fortunately for them, iRules is quite capable. First let's take a look at the different portions of the problem, then we'll get into some code:

1. First, if a user makes a request in the old domain + URI format, it needs to be redirected to the new domain.
2. Next, if a user makes a request to one of the new domains, an invisible rewrite needs to happen to feed the back-end server the old format (old domain + URI)
3. Because of the above invisible rewrite, any responses coming back from the server need to be inspected to see if the Location header contains the old domain + URI format. Those that match need to be rewritten to the appropriate new domain, instead.

Okay, let's dive into each task in more detail to try and make more sense out of it.

1. Redirect

We're looking to change a client request for domain.com/101 to a.com (made up values, obviously). I.E., any time a user requests the old domain + URI format, we're going to redirect them to the new domain that matches the URI they provided. To do this, we'll use a host header check, a class match, and a redirect. First create a class that creates the appropriate mappings like:

/101 -> a.com
/102 -> b.com
/103 -> c.com
… etc.

Note: The above is not a properly formatted class, it's just the logical flow. Once that's created, this section looks like:

   1: if {[HTTP::host] eq "domain.com"} {
   2:   set redirDom [class match -value [HTTP::path] starts_with redirect_urls]
   3:   if {$redirDom ne ""} {
   4:     HTTP::redirect "https://$redirDom"
   5:     unset redirDom
   6:   }
   7: }

As you can see we make sure the that the original request was bound for the old domain, then we perform a class match to get the value associated with the URI being requested. Given our class format above, this will map to the new domain name that we want to redirect the user to. Once we have that value, we perform the redirect and the user will see the new format, hopefully helping to reinforce the new proedure.

2. Invisible Rewrite

There's a big difference between a redirect and a rewrite. A redirect responds to the user, telling them to try a different location to get the content they want. A rewrite simple changes the location that the request is actually bound for, without ever alerting the user. This is important in this case, because we don't want the users to see the old format. To achieve this, we're going to need a different class that is basically the exact opposite of the one above, so the logical layout of the class is:

a.com -> /101
b.com -> /102
c.com -> /103

Once that is created and properly formatted, the logic for the rewrite is actually pretty simple. It looks like:

   1: set newURI [class match -value [HTTP::host] eq rewrite_domains]
   2: if {$newURI ne ""} {
   3:   HTTP::uri $newURI
   4:   HTTP::header replace "Host" "domain.com"
   5:   unset newURI
   6: }

All we're doing here is checking to see if the domain being requested by the client matches the list of new domains that needs to be rewritten into the old format for the server's sake. If there's a match, we retrieve the value in the class again, this time it will map to the URI at which the desired app resides. Once we have that, we silently replace the URI with the new one, and replace the host header's value with the old domain instead of the new one.

3. Location Modification

Last but not least, now that we're monkeying with the headers to achieve the invisible rewrite functionality we want, we have to catch the responses on the way back to the user and make sure that they don't see the server responding with inappropriate data. Because the server actually saw a request come in for "domain.com/101", it will respond with that as the destination in the Location header. We don't want that, we want the user to receive a location header that, in this case, reads "a.com". So, another class lookup, another header replace, and we're there. This time we can re-use the class created above in the rewrite section. This chunk of logic looks like:

   1: if {([HTTP::header exists "Location"]) && ([HTTP::header "Location"] contains "domain.com")} {
   2:   set loc [string tolower [string trimright [HTTP::header "Location"] "/"]]
   3:   set newDom [class match -value $loc ends_with redirect_uls]
   4:   if {$newDom ne ""} {
   5:     HTTP::header replace "Location" "https://$newDom"
   6:   }
   7: }

Obviously there are some assumptions here, such as HTTPS being the preferred protocol, the URI not being required as a carry over, etc. All of those things can be tweaked as necessary, but this gives you the basic concept.

Once you assemble all three sections, you'll have an iRule that achieves all of the above requirements without much overhead, thanks to the efficiency of the class command. The finished product looks like:

   1: when HTTP_REQUEST {
   2:   # Check to see if this is a new domain that needs to be silently re-written
   3:   set newURI [class match -value [HTTP::host] eq rewrite_domains]
   4:   if {$newURI ne ""} {
   5:     # If it is, re-write the URI to the class matched value, and the host header to the old domain
   6:     HTTP::uri $newURI
   7:     HTTP::header replace "Host" "domain.com"
   8:     unset newURI
   9:   # If it's not, check to see if it's someone accessing the old domain incorrectly
  10:   } elseif {[HTTP::host] eq "domain.com"} {
  11:     # If it is someone accessing the old domain format, redirect them to the new domain
  12:     set redirDom [class match -value [HTTP::path] starts_with redirect_urls]
  13:     if {$redirDom ne ""} {
  14:       HTTP::redirect "https://$redirDom"
  15:       unset redirDom
  16:     }
  17:   }
  18: }
  20: when HTTP_RESPONSE {
  21:   # If a Location header exists, check to see if it contains the old domain
  22:   if {([HTTP::header exists "Location"]) && ([HTTP::header "Location"] contains "domain.com")} {
  23:     # If so, check whether we it's one of the re-write URIs
  24:     set loc [string tolower [string trimright [HTTP::header "Location"] "/"]]
  25:     set newDom [class match -value $loc ends_with redirect_uls]
  26:     # If it is, replace the Location header with the new domain instead
  27:     if {$newDom ne ""} {
  28:       HTTP::header replace "Location" "https://$newDom"
  29:     }
  30:   }
  31: }
Comments on this Article
Comment made 07-Jun-2012 by anuj 0
Thanks for the article Colin. The problem situation sounds very familiar and that makes this article more useful and not just a hypothetical write up
Comment made 15-Jan-2014 by ben wyatt
Hi - looks great - is just the solution I am looking for :)

Being very cheeky - I dont suppose you could post one that has:

old.domain.com and new.domain.com so I can just copy the irule and change it to my domains?

I have tried modifying it but I cannot get it to work, mainly because I am not sure what I should be changing....

Thanks a lot for your help,