Forum Discussion

Jay_213729's avatar
Jay_213729
Icon for Nimbostratus rankNimbostratus
Sep 21, 2015

Modify SOAP To header based on target node

Hi, I have 2 kinds of virtual servers set up. The first has no pools assigned. It takes requests over SSL, terminates the SSL and routes the requests (in plaintext) to wholly different pools based on content.

However, the SOAP To header from these requests needs to be modified. The protocol needs to be changed from https to http, the service URL is suffixed with a designator used for services without transport security and the port needs to be changed from 8443 to some other port.

It's the "some other port" that is causing me some issues. Following is my iRule on the SSL-terminating, content-based routing virtual server:

when HTTP_REQUEST {
  if { [HTTP::method] eq "POST" } {
    if { [HTTP::header exists "Content-Length"] } {
      set content_length [HTTP::header "Content-Length"]
    } else {
      set content_length 1048576
    }

    HTTP::collect $content_length
  } else {
    HTTP::respond 405 content "Unsupported" Allow "POST"
  }
}

when HTTP_REQUEST_DATA {
  if { [HTTP::method] eq "POST" } {
    set payload [HTTP::payload]

    binary scan [sha1 [SSL::cert 0]] H* certHash

    set newSoapHeader ""

    if { [info exists certHash] } {
      append newSoapHeader $certHash
    } else {
      append newSoapHeader "cert_hash_retrieval_failure"
    }

    append newSoapHeader ""

    Try to stick the new header in the header collection
    set numMatches [regsub {.*\<[A-Za-z0-9:]*Header[^\>]*\>} $payload [concat {&} $newSoapHeader] modifiedPayload]

    if no matches from that, we know there was no header and, therefore, no subsitution. We need to introduce the whole Header block
    if { $numMatches == 0 } {
      set contentToInsert ""
      append contentToInsert $newSoapHeader
      append contentToInsert ""

      set numMatches [regsub {.*\<[A-Za-z0-9:]*Envelope[^\>]*\>} $payload [concat {&} $contentToInsert] modifiedPayload]
    }

    Empty it of content first
    HTTP::payload replace 0 [string length [HTTP::payload]] ""
    Then replace the empty payload
    HTTP::payload replace 0 0 $modifiedPayload

    set currentVirtualServer [virtual name]

    if { [string match *LEGACY* $currentVirtualServer] > 0 } {
      Replace the HTTP Location Header
      HTTP::header replace Location [string map { "https://" "http://" ".svc" "_U.svc" } [ HTTP::header Location]]
      Replace the SOAP To Header
      set badToHeaderPattern ".svc"
      set lenBadHeaderPattern [string length $badToHeaderPattern]
      set betterToHeaderPattern "_U.svc"

      set offset [string first $badToHeaderPattern [HTTP::payload]]

      if { $offset >= 0 } {
        HTTP::payload replace $offset $lenBadHeaderPattern $betterToHeaderPattern

        set badToHeaderPattern "https://"
        set lenBadHeaderPattern [string length $badToHeaderPattern]
        set betterToHeaderPattern "http://"

        set offset [string first $badToHeaderPattern [HTTP::payload]]

        if { $offset >= 0 } {
          HTTP::payload replace $offset $lenBadHeaderPattern $betterToHeaderPattern

        }

        NOW REPLACE THE 8443 PORT WITH... um...
      }
    }

    the trailing arguments align with the parts of the regex in parentheses. First is always the whole match, subsequent per parentheses pair
    regexp {(\<[A-Za-z0-9:]*Organisation[^\>]*\>)([A-Za-z]*)} $modifiedPayload wholeMatch xmlElementMatch organisationNameMatch

    if { [string length $organisationNameMatch] > 0 } {
      string map takes a list of replacement pairs (e.g [list needle1 replace1 needle2 replace2... needleN replaceN])
      set targetPool [string map [list -D--- --- MULTI $organisationNameMatch RELEASE RELEASE-U DEBUG DEBUG-U LEGACY ""] $currentVirtualServer]

      pool $targetPool
    } else {
      HTTP::respond 400 content "Unknown"
    }
  }

  Ensure that the request is released back to F5 so it may take control of the underlying connection and complete the routing
  HTTP::release
}

I'll detail the HTTP_REQUEST if requested, but it's pretty much stock-standard stuff. All the magic is in the HTTP_REQUEST_DATA handling. I know a lot of my SOAP content replacement is really risky and needs better pattern matching/controls so I don't replace anything I shouldn't, so don't focus on that. But after I switch out the https with http in the SOAP To header (really every instance of https in the whole payload), I want to change the port 8443 to another port. The other port, however, is not known until I route it to the pool later (the "pool $targetPool" in the last couple of lines).

That routing works just fine and the correct pool gets the requests and processes them. But I can't seem to manipulate the payload any further after I set the new pool. That means I can't, say, get "[LB::server port]" and replace the mentions of 8443 with it. It looks like I don't have it available to manipulate on the LB_SELECTED event either.

So, how do I modify the port in the SOAP To Header (also need to modify the HTTP Location header) with a port I will only know after I've re-routed the request with the "pool" keyword?

  • Thought I'd return to answer my own question for posterity and in the hopes it might be of some help to those who come after me.

     

    I've split the iRule into two. The first iRule will add the certificate thumbprint as a new SOAP header to the payload and inspect the "Organisation" element in the payload to figure out how to route. Instead of routing to a pool, however, it now routes to a virtual server (virtual [servername]). This gives me a hook to introduce a second iRule that I've attached to the target virtual server (the one we routed to in iRule 1).

     

    The second iRule handles the CLIENT_ACCEPTED event and stores the [TCP::local_port] in a variable which is later used in the HTTP_REQUEST_DATA handler when I insert the SOAP To header. Probably worth noting that I'm also replacing the Host and Location HTTP headers, but this really has no bearing on inserting some or all of the final virtual server's address in the payload.

     

    One last thing I'll mention is the content-length header. I ended up storing it away during the HTTP_REQUEST_DATA handler (after I've made all payload modifications, of course) and then I trap the HTTP_REQUEST_SEND and insert the content-length header with the new payload length. I do this on BOTH iRules. That is, the first iRule will upsert the header (clients may include it) with the new length and the second iRule inserts (because it seems to disappear) the header with a re-calculated length (set payloadLength [HTTP::payload length]).

     

    And so I commit this to the void.

     

  • Hey! Thanks DevCentral!! A Jumbo Badge for 100 views! I'll wear it proud.