Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with:

Intercepting "ColdFusion As A Service" SOAP And RESTful Component Requests

By Ben Nadel on
Tags: ColdFusion

Warning: What you are about to read might not hold any value; but, it took me over ten hours to figure out and I think it touches on some really cool stuff, so I'm gonna post about it anyway. As you have probably heard, ColdFusion 9 introduced the concept of "ColdFusion as a Service" to open up ColdFusion functionality to non-ColdFusion technologies. It does this by providing web-accessible, encrypted, user-based ColdFusion components that wrap around ColdFusion features like CFMail, CFDocument, CFImage, and CFPOP (to name a few). Because I'm already using ColdFusion, this didn't interest me too much as a product feature; but, what it did represent to me was a new sandbox to play in. And, play I did!

When I saw the ColdFusion as a Service stuff, I thought it would be cool to come up with a way to intercept all of the requests made to the service components in case I wanted to manage them in some way. For example, maybe I want to throw a watermark on any image produced or throttle people's use of CFMail based on their username and password. The easiest was to do such a thing would be to create your own service objects that extend the original service objects and intercept them that way. But, honestly, that's much less fun (although decidedly much more efficient) than intercepting the request itself!

By intercepting the ColdFusion as a Service requests, we can explore some much more interesting problem spaces. Let's face it, the ColdFusion application framework is so powerful that intercepting standard HTTP requests is almost a non-issue; but, because the ColdFusion framework does so much behind the scenes when it comes to SOAP requests, intercepting those presents a whole new slew of problems. If you've worked with SOAP requests before, you'll know that even calling GetHTTPRequestData() during a SOAP request corrupts the SOAP response - like I said, very interesting stuff!

To experiment with this as a sort of proof of concept, I wanted to add a "Provided by Ben Nadel" watermark to every image that was produced by my "ColdFusion as a Service" Image component. Like I said above, the hardest part about this experiment is going to be intercepting the SOAP-based requests because they are far more complicated than standard HTTP requests. But, before we take a look at what's going on behind the scenes, let's take a look at the test code that is working off of my ColdFusion as a Service components. In the following demo, I am uploading two photos using CFHTTP and the Upload.cfc service. Then, using both CFHTTP and SOAP Web Service calls, I scale the uploaded images to fit in a 350 x 350 box.

  • <!---
  • Get the HTTP directory for our ColdFusion 9 services.
  • Remember, we are trying to simulate a server that is NOT
  • as cool as ColdFusion.
  • --->
  • <cfset serviceURL = "http://localhost:8501/cfide/services/" />
  •  
  • <!---
  • Set the username and password for the ColdFusion as a
  • Service components.
  •  
  • NOTE: This had to be set up in the ColdFusion administrator
  • before it could be used for the service.
  • --->
  • <cfset username = "cfaas" />
  • <cfset password = "cfaas" />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • First thing we need to do is upload a few images to the server
  • so that we can start playing around with them. One image will
  • be consuming the ColdFusion as a Service components using
  • standard CFHTTP calls and one will be consuming them using
  • SOAP-based web service calls.
  •  
  • This post will upload all photos in the FORM and will return
  • an array of URLs of the uploaded image as a WDDX packet:
  •  
  • <wddxPacket version='1.0'>
  • <header/>
  • <data>
  • <array length='1'>
  • <struct type='coldfusion.runtime.TemplateProxy'>
  • <var name='VALUE'>
  • <string>
  • [[ YOUR IMAGE URL HERE ]]
  • </string>
  • </var>
  • <var name='KEY'>
  • <string>
  • girl.jpg
  • </string>
  • </var>
  • </struct>
  • </array>
  • </data>
  • </wddxPacket>
  •  
  • NOTE: If you try to CFDump out the response to this HTTP post,
  • you will get "[undefined array element]". This does NOT mean
  • that the post failed - ColdFusion simply is having trouble
  • de-serializing its own WDDX format (funky).
  •  
  • Notice also that the upload does not require a username and
  • password (as far as I can tell).
  • --->
  • <cfhttp
  • result="post"
  • url="#serviceURL#upload.cfc?method=uploadForm"
  • method="post">
  •  
  • <!--- Upload the first local photo. --->
  • <cfhttpparam
  • type="file"
  • name="tanya1"
  • file="#expandPath( './tanya1.jpg' )#"
  • />
  •  
  • <!--- Upload the second local photo. --->
  • <cfhttpparam
  • type="file"
  • name="tanya2"
  • file="#expandPath( './tanya2.jpg' )#"
  • />
  •  
  • </cfhttp>
  •  
  • <!---
  • Now that we have upoaded our photos, we have to parse the
  • new URLs from our WDDX response. Since ColdFusion cannot
  • seem to de-serialize this WDDX, we need to gather the VALUE
  • nodes using XPath.
  • --->
  • <cfset valueNodes = xmlSearch(
  • post.fileContent,
  • "//struct/var[ @name = 'VALUE' ]/string/text()"
  • ) />
  •  
  • <!--- Let's store the path from the value nodes. --->
  • <cfset photoURL1 = valueNodes[ 1 ].xmlValue />
  • <cfset photoURL2 = valueNodes[ 2 ].xmlValue />
  •  
  • <!---
  • Let's output the Two uploaded photos using the new URLs
  • (these are large images, so I'm using an explicit width).
  • --->
  • <cfoutput>
  •  
  • <p>
  • Uploaded Photo 1:<br />
  • <img src="#photoURL1#" width="350" />
  • </p>
  •  
  • <p>
  • Uploaded Photo 2:<br />
  • <img src="#photoURL2#" width="350" />
  • </p>
  •  
  • </cfoutput>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have our uploaded photos, let's start using the
  • ColdFusion as a Service components to maniupate them. For the
  • first one, I'm going to use standard CFHTTP calls (restful
  • access) and for the second one, I'm going to use SOAP-based
  • calls (web service access).
  • --->
  •  
  • <h2>
  • RESTful CFHTTP Approach (via CFHTTP)
  • </h2>
  •  
  • <!---
  • Let's pass this image through the ScaleToFit image service.
  • This time, we need to pass in our username and password as
  • part of the method invocation. This CFHTTP request will
  • return the new URL of the manipulated photo in a WDDX response:
  •  
  • <wddxPacket version='1.0'>
  • <header/>
  • <data>
  • <string>
  • [[ YOUR IMAGE URL HERE ]]
  • </string>
  • </data>
  • </wddxPacket>
  •  
  • NOTE This will return a NEW photo url - the original, uploaded
  • photo remains untouched.
  •  
  • NOTE: Our "source" argument is one of the photo URLs that
  • we received from the upload above.
  • --->
  • <cfhttp
  • result="post"
  • url="#serviceURL#image.cfc?method=scaletoFit"
  • method="post">
  •  
  • <cfhttpparam
  • type="formfield"
  • name="serviceUsername"
  • value="#username#"
  • />
  •  
  • <cfhttpparam
  • type="formfield"
  • name="servicePassword"
  • value="#password#"
  • />
  •  
  • <cfhttpparam
  • type="formfield"
  • name="source"
  • value="#photoURL1#"
  • />
  •  
  • <cfhttpparam
  • type="formfield"
  • name="fitWidth"
  • value="350"
  • />
  •  
  • <cfhttpparam
  • type="formfield"
  • name="fitHeight"
  • value="350"
  • />
  •  
  • </cfhttp>
  •  
  • <!---
  • Now that we have scaled our photo, we have to get the new URL.
  • This WDDX package ColdFusion can handle so all we have to do
  • is deserialized the WDDX.
  • --->
  • <cfwddx
  • output="resizedPhotoUrl1"
  • action="wddx2cfml"
  • input="#post.fileContent#"
  • />
  •  
  • <!--- Let's output the resized photo using the new URL. --->
  • <cfoutput>
  •  
  • <p>
  • Resized Photo 1:<br />
  • <img src="#resizedPhotoUrl1#" />
  • </p>
  •  
  • </cfoutput>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • For our second approach, we are going to create a webservice
  • using CreateObject(). This will invoke our ColdFusion as a
  • Service components using SOAP-based requests.
  • --->
  •  
  • <h2>
  • SOAP Web Service Approach (via CreateObject)
  • </h2>
  •  
  • <!---
  • First, we need to create a SOAP web service object for our
  • Image component. Notice that we have to pass in the WSDL-
  • URL when creating our web service.
  • --->
  • <cfset imageService = createObject(
  • "webservice",
  • "#serviceURL#image.cfc?wsdl"
  • ) />
  •  
  • <!---
  • Now that we have our web service object, we can invoke the
  • scaleToFit() method directly on it.
  •  
  • NOTE: In this version, we have to send in two arguments that
  • we didn't send before because "omitting" web service arguments
  • is a total pain without using the CFInvokeArgument tags.
  •  
  • NOTE: This returned the actual string value of the new URL
  • since ColdFusion does so much auto serialization and
  • de-serializeation behind the scenes.
  • --->
  • <cfset resizedPhotoUrl2 = imageService.scaleToFit(
  • serviceUsername = username,
  • servicePassword = password,
  • source = photoURL2,
  • fitWidth = 350,
  • fitHeight = 350,
  • interpolation = "highestQuality",
  • blurFactor = "1"
  • ) />
  •  
  • <!--- Let's output the resized photo using the new URL. --->
  • <cfoutput>
  •  
  • <p>
  • Resized Photo 2:<br />
  • <img src="#resizedPhotoUrl2#" />
  • </p>
  •  
  • </cfoutput>

As you can see, one uploaded image is resized using CFHTTP and one uploaded image is resized using a SOAP web service object (created via CreateObject()). When we run the above code, we get the following page output:

 
 
 
 
 
 
Intercepting  
 
 
 

As you can see from the page output, both photos that were resized - RESTful or SOAP - were given the "Provided by Ben Nadel" watermark. Sweeet!

Ok, so how is this working? Well, in order to intercept any ColdFusion as a Service requests, we need to create a single point-of-entry into the service components. Because I didn't want to mess with the components themselves, this could only be accomplished by adding an Application.cfc ColdFusion application framework component. With an Application.cfc component in place, suddenly every request would be funnelled through the OnRequestStart() application event handler, at which point we could manually redirect it, manipulate it, or do whatever we wanted to it.

As powerful as Application.cfc and the ColdFusion framework is, it still does not give us too much help with SOAP-based requests. As such, SOAP requests require much more fenagling. The only thing I could figure out for this was to take a SOAP request, manually re-post it back to the application, receive the SOAP response, then parse it as XML, and return the XML onto the client. A pain in the butt, but it seemed to work seamlessly.

Here's a graphical representation of the Application.cfc page request model:

 
 
 
 
 
 
Intercepting  
 
 
 

If the incoming request is a standard HTTP (RESTful) request, I route it through the OnCFCRequest() event handler, where the web service call is made locally and the response returned to the client. If the incoming request is a SOAP request, I repost it, then pass the SOAP request and SOAP response off to the OnSOAPRequst() event handler, where the response is returned to the client. In either case, the final event handler takes the resultant image URL and applies a watermark before responding to the client.

Application.cfc

  • <!---
  • This Application component extends the base Service
  • component so that it can gain access to the utility methods
  • for URL-to-Path conversion.
  • --->
  • <cfcomponent
  • extends="base"
  • output="false"
  • hint="I provide application settings and event handlers.">
  •  
  • <!---
  • While I don't think that the root Application.cfm
  • (really?? Still using CFM??) is important, let's include
  • it anyway. Since we can include it without any error, it's
  • probably a good sign it wasn't doing anything too
  • important like defining CFApplication.
  • --->
  • <cfinclude template="../Application.cfm" />
  •  
  • <!---
  • Make sure this kind of request does not page request
  • debugging output.
  • --->
  • <cfsetting showdebugoutput="false" />
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the request.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="script"
  • type="string"
  • required="true"
  • hint="I am the script being requested."
  • />
  •  
  • <!---
  • The very first thing we want to do is check to see if
  • the allowSOAPProcessing cookie exists; this will be
  • there if the current request was re-posted by the
  • *same* request. In this case, we want to make sure the
  • SOAP request just processes as ColdFusion would
  • normally handle it.
  • --->
  • <cfif !isNull( cookie.allowSOAPProcessing )>
  •  
  • <!---
  • Return out with true to let the SOAP page be
  • processed as it would naturally.
  • --->
  • <cfreturn true />
  •  
  • </cfif>
  •  
  • <!---
  • Now that we know that we're not dealing with a re-
  • posted SOAP request, let's check to see if we are
  • dealing with an initial SOAP request.
  • --->
  • <cfset request.isSOAPRequest = this.isWSDLRequest() />
  •  
  •  
  • <!---
  • Let's merge the URL and FORM attributes into a single
  • scope for easy of reference.
  • --->
  • <cfset request.attributes = duplicate( url ) />
  •  
  • <!---
  • Check to see if the FORM exists. If this is a SOAP-
  • based request, there is no FORM scope.
  • --->
  • <cfif !request.isSOAPRequest>
  • <cfset structAppend( request.attributes, form ) />
  • </cfif>
  •  
  •  
  • <!---
  • Check to see if we are dealing with a SOAP request.
  • If so, we want to actually re-post such that we can
  • let ColdFusion deal with it naturally and then
  • intercept the SOAP XML response, which we can then
  • return ourselves.
  • --->
  • <cfif request.isSOAPRequest>
  •  
  • <!---
  • Re-post the SOAP request. This will return a
  • request and response XML structures.
  • --->
  • <cfset local.repost = this.repostSOAPRequest() />
  •  
  • <!---
  • Now that we have the re-posted SOAP request and
  • response, let's wire the CFC request handler to
  • be the SOAP-based request handler. Rather than
  • just re-wiring, which will cause an error, let's
  • explicitly call the SOAP handler with the re-
  • posted response.
  • --->
  • <cfset this.onSOAPRequest(
  • arguments.script,
  • local.repost.request,
  • local.repost.response
  • ) />
  •  
  • <!---
  • For non-SOAP requests, we still want to make sure
  • the method is defined. If not, just let ColdFusion
  • perform a Component Inspection.
  • --->
  • <cfelseif structKeyExists( request.attributes, "method" )>
  •  
  • <!---
  • This is not a SOAP request; as such, let's wire
  • up our CFC-based request handler so that it
  • manually executes the CFC processing.
  • --->
  • <cfset this.onCFCRequest = this.$onCFCRequest />
  • <cfset variables.onCFCRequest = variables.$onCFCRequest />
  •  
  • </cfif>
  •  
  • <!--- Return true so the request will process. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <!---
  • When necessary, this will become the OnCFCRequest() event
  • handler. But, since we will be dealing with SOAP requests
  • occassionally, we are going to leave it un-wired to start
  • with (notice leading "$" sign).
  • --->
  • <cffunction
  • name="$onCFCRequest"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I wrangle the ColdFusion service requests (when I am manually wired up).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="cfcClass"
  • type="string"
  • required="true"
  • hint="I am the component class being requestsed."
  • />
  •  
  • <cfargument
  • name="cfcMethod"
  • type="string"
  • required="true"
  • hint="I am the name of the method being invoked."
  • />
  •  
  • <cfargument
  • name="cfcArguments"
  • type="struct"
  • required="true"
  • hint="I am the name of the method being invoked."
  • />
  •  
  •  
  • <!---
  • Invoke the method, passing the given arguments off to
  • the given method.
  •  
  • NOTE: Some methods like uploadForm() do not actually
  • take arguments (they read the FORM scope directly),
  • but passing in the arguments generically will not
  • hurt anything.
  • --->
  • <cfinvoke
  • returnvariable="local.result"
  • component="#arguments.cfcClass#"
  • method="#arguments.cfcMethod#"
  • argumentcollection="#arguments.cfcArguments#"
  • />
  •  
  •  
  • <!---
  • Now that we have our service response, let's pass the
  • result to any interceptor that we have.
  • --->
  • <cfif reFindNoCase( "image$", arguments.cfcClass )>
  •  
  • <!---
  • Image Service Class; check to see if the given
  • methods need to be intercepted.
  • --->
  • <cfif listFindNoCase( "AddBorder Blur Crop Flip GrayScale Negative Overlay Resize Rotate ScaleToFit Sharpen Shear", arguments.cfcMethod, " " )>
  •  
  • <!--- Apply the watermark to the new image. --->
  • <cfset this.applyWatermark(
  • this.convertURLtoPath( local.result )
  • ) />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have our result, we have to figure out
  • how we want to package the return value. By default
  • (I am guessing) all of the ColdFusion services are
  • designed to return WDDX. To make this check easier,
  • let's default the request-based property to WDDX.
  • --->
  • <cfparam
  • name="request.attributes.returnFormat"
  • type="string"
  • default="wddx"
  • />
  •  
  • <!---
  • Now that our returnFormat is defaulted, let's check
  • to see what format has been requested.
  • --->
  • <cfif (request.attributes.returnFormat eq "json")>
  •  
  • <!--- Serialize to JSON. --->
  • <cfset local.responseData = serializeJSON(
  • local.result
  • ) />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "application/x-json" />
  •  
  • <cfelse>
  •  
  • <!--- Serialize the request to WDDX. --->
  • <cfwddx
  • output="local.responseData"
  • action="cfml2wddx"
  • input="#local.result#"
  • />
  •  
  • <!--- Set the appropriate mime type. --->
  • <cfset local.responseMimeType = "text/xml" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • To make setting the streaming control easier, let's
  • convert to binary. This will let us terminate the
  • request without using any abort statements.
  • --->
  • <cfset local.responseBinary = toBinary(
  • toBase64(
  • local.responseData
  • )
  • ) />
  •  
  •  
  • <!--- Set the response length. --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( local.responseBinary )#"
  • />
  •  
  • <!--- Stream the content back to the client. --->
  • <cfcontent
  • type="#local.responseMimeType#"
  • variable="#local.responseBinary#"
  • />
  •  
  • <!--- --------------------------------------------- --->
  • <!--- --------------------------------------------- --->
  •  
  • <!---
  • ASSERT: The request will never get this far since
  • the use of the CFContent tag will have streamed
  • the content and finalized the request.
  • --->
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onSOAPRequest"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I handle the SOAP-based request (more like the response - the request itself is implicitly processed by ColdFusion and the AXIS library).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="script"
  • type="string"
  • required="true"
  • hint="I am the script being requested."
  • />
  •  
  • <cfargument
  • name="request"
  • type="string"
  • required="true"
  • hint="I am the SOAP XML request submitted to the given script for the current request."
  • />
  •  
  • <cfargument
  • name="response"
  • type="string"
  • required="true"
  • hint="I am the SOAP XML response returned by the given script for the current request."
  • />
  •  
  •  
  • <!---
  • Now that we have our service response, let's pass the
  • result to any interceptor that we have.
  • --->
  • <cfif reFindNoCase( "image\.cfc$", arguments.script )>
  •  
  • <!---
  • Image Service Class; check to see if the given
  • methods need to be intercepted.
  • --->
  • <cfset local.returnNodes = xmlSearch(
  • arguments.response,
  • "//*[ contains( local-name(), 'Return' ) ]"
  • ) />
  •  
  • <!--- Check to see if the return node was found. --->
  • <cfif arrayLen( local.returnNodes )>
  •  
  • <!---
  • Check to see if the given method requires
  • intercepting. We can base this off of the name
  • of the Return node.
  • --->
  • <cfif listFindNoCase( "AddBorderReturn BlurReturn CropReturn FlipReturn ReturnGrayScaleReturn NegativeReturn OverlayReturn ResizeReturn RotateReturn ScaleToFitReturn SharpenReturn ShearReturn", local.returnNodes[ 1 ].xmlName, " " )>
  •  
  • <!--- Apply the watermark to the new image. --->
  • <cfset this.applyWatermark(
  • this.convertURLtoPath(
  • local.returnNodes[ 1 ].xmlText
  • )
  • ) />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • To make setting the streaming control easier, let's
  • convert the XML resposne to binary. This will let us
  • terminate the request without using any abort
  • statements.
  • --->
  • <cfset local.responseBinary = toBinary(
  • toBase64(
  • arguments.response
  • )
  • ) />
  •  
  •  
  • <!--- Set the response length. --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( local.responseBinary )#"
  • />
  •  
  • <!--- Stream the content back to the client. --->
  • <cfcontent
  • type="text/xml"
  • variable="#local.responseBinary#"
  • />
  •  
  • <!--- --------------------------------------------- --->
  • <!--- --------------------------------------------- --->
  •  
  • <!---
  • ASSERT: The request will never get this far since
  • the use of the CFContent tag will have streamed
  • the content and finalized the request.
  • --->
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onError"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I handle uncaught exceptions.">
  •  
  • <cfdump
  • var="#arguments#"
  • format="html"
  • output="#expandPath( './error.htm' )#"
  • />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <cffunction
  • name="applyWatermark"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I apply a watermak to the image at the given file.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="filePath"
  • type="any"
  • required="true"
  • hint="I am the file path to the image being manipulated in the image service request."
  • />
  •  
  • <!---
  • Read in the watermark image.
  •  
  • NOTE: Relateive file paths to imageNew() will be
  • relative to the current template (ie. the services
  • directory).
  • --->
  • <cfset local.watermark = imageNew( "./watermark.gif" ) />
  •  
  • <!--- Read in the service image. --->
  • <cfset local.target = imageNew( arguments.filePath ) />
  •  
  • <!--- Paste the watermark onto the target. --->
  • <cfset imagePaste(
  • local.target,
  • local.watermark,
  • (imageGetWidth( local.target ) - imageGetWidth( local.watermark )),
  • (imageGetHeight( local.target ) - imageGetHeight( local.watermark ))
  • ) />
  •  
  • <!---
  • Write the target back to file; this will override
  • the image that was produced by the ColdFusion as a
  • Service request.
  • --->
  • <cfset imageWrite(
  • local.target,
  • arguments.filePath
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="isWSDLRequest"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I determine if the current request is a SOAP request (as best I can).">
  •  
  • <!---
  • There's no great way to check to see if this is a
  • SOAP request; but, if there is no FORM scope, then
  • this is a SOAP request.
  • --->
  • <cfreturn !isDefined( "form" ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="repostSOAPRequest"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I repost the current SOAP request and return the XML Soap response.">
  •  
  • <!--- Get the current request data. --->
  • <cfset local.httpData = getHTTPRequestData() />
  •  
  • <!--- Get the original host name of the SOAP post. --->
  • <cfif structKeyExists( local.httpData.headers, "host" )>
  •  
  • <!--- Grab the original host. --->
  • <cfset local.httpHost = local.httpData.headers.host />
  •  
  • <cfelse>
  •  
  • <!---
  • If there is no host header available, just use
  • the CGI-based value.
  • --->
  • <cfset local.httpHost = "#cgi.server_name#:#cgi.server_port#" />
  •  
  • </cfif>
  •  
  • <!---
  • Now that we have the original host, delete the one
  • that was posted-in. We need to re-post to a local
  • HTTP address and in order for the SOAP to wire
  • properly, we need to change the host value.
  • --->
  • <cfset structDelete( local.httpData.headers, "host" ) />
  •  
  • <!---
  • Build the re-post URL. When doing this, we have to
  • check to see if we need to append the WSDL flag to
  • the URL.
  • --->
  • <cfset local.repostUrl = (
  • "http://127.0.0.1:#cgi.server_port##cgi.script_name#" &
  • (!isNull( url.wsdl ) ? "?wsdl" : "")
  • ) />
  •  
  • <!--- Repost back to the local machine. --->
  • <cfhttp
  • result="local.post"
  • url="#local.repostUrl#"
  • method="#local.httpData.method#">
  •  
  • <!--- Repost the current headers. --->
  • <cfloop
  • item="local.headerKey"
  • collection="#local.httpData.headers#">
  •  
  • <cfhttpparam
  • type="header"
  • name="#local.headerKey#"
  • value="#local.httpData.headers[ local.headerKey ]#"
  • />
  •  
  • </cfloop>
  •  
  • <!---
  • Repose the SOAP body as the content.
  •  
  • NOTE: For WSDL-definition requests, this will be
  • empty (and the method will be "get", not "post").
  • --->
  • <cfif len( local.httpData.content )>
  •  
  • <cfhttpparam
  • type="xml"
  • value="#local.httpData.content#"
  • />
  •  
  • </cfif>
  •  
  • <!---
  • Post the cookie flag to make sure the re-post gets
  • processed as a standard SOAP request without any
  • intercepting.
  • --->
  • <cfhttpparam
  • type="cookie"
  • name="allowSOAPProcessing"
  • value="true"
  • />
  •  
  • </cfhttp>
  •  
  • <!---
  • Now that have the SOAP resposne, we have to replace
  • out any references to the local host with the host
  • that was passed-in.
  • --->
  • <cfset local.soapResponse = replace(
  • local.post.fileContent,
  • "127.0.0.1:#cgi.server_port#",
  • local.httpHost,
  • "all"
  • ) />
  •  
  • <!--- Return the SOAP request and response. --->
  • <cfreturn {
  • request = local.httpData.content,
  • response = local.soapResponse
  • } />
  • </cffunction>
  •  
  • </cfcomponent>

I tried to comment this Application.cfc component as best as I could, so I will refrain from giving much additional explanation. The bottom line, however, is that this worked! It successfully intercepts both SOAP and RESTful style page requests and provides a way for the ColdFusion application framework to mess with the "ColdFusion as a Service" responses before they are actually returned to the client.

This whole approach was just an experiment - it wasn't meant to be a best practice in any way; I was just trying to push the limits of the ColdFusion application framework. But, I just know that someone is going to skip over the entire explanation, head to the code, and then jump in with a comment on how it would be much easier to simply extend the existing service components and intercept the requests that way. To head that off, I threw together a small demo of that approach. In the following scenario, I renamed the Image.cfc service component to OriginalImage.cfc and created my own Image.cfc which extends the former:

Image.cfc (My Sub-Class Version)

  • <!---
  • Notice that my version of the "ColdFusion as a Service"
  • Image component extends the original, renamed version.
  • --->
  • <cfcomponent
  • extends="OriginalImage"
  • output="false"
  • hint="I provided 'managed' Image service requests.">
  •  
  •  
  • <!--- Make sure there is no debugging output. --->
  • <cfsetting showdebugoutput="false" />
  •  
  •  
  • <!---
  • For this demo, I am only overriding one method. For
  • full management, you would have to manually override
  • all of the remote-access methods.
  • --->
  • <cffunction
  • name="scaleToFit"
  • access="remote"
  • returntype="string"
  • returnformat="wddx"
  • output="false"
  • hint="I scale an image to fit in the given box.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="serviceusername" type="string" />
  • <cfargument name="servicepassword" type="string" />
  • <cfargument name="source" type="string" />
  • <cfargument name="fitWidth" type="string" />
  • <cfargument name="fitHeight" type="string" />
  • <cfargument name="interpolation" type="string" />
  • <cfargument name="blurfactor" type="string" />
  •  
  • <!---
  • Pass the arguments off to the SUPER object (the
  • original Image.cfc service component).
  • --->
  • <cfset local.result = super.scaleToFit(
  • argumentCollection = arguments
  • ) />
  •  
  • <!--- Apply the watermark to the response iamge. --->
  • <cfset this.applyWatermark(
  • this.convertURLtoPath(
  • local.result
  • )
  • ) />
  •  
  • <!--- Return the new image URL. --->
  • <cfreturn local.result />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="applyWatermark"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I apply a watermak to the image at the given file.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="filePath"
  • type="any"
  • required="true"
  • hint="I am the file path to the image being manipulated in the image service request."
  • />
  •  
  • <!---
  • Read in the watermark image.
  •  
  • NOTE: Relateive file paths to imageNew() will be
  • relative to the current template (ie. the services
  • directory).
  • --->
  • <cfset local.watermark = imageNew( "./watermark.gif" ) />
  •  
  • <!--- Read in the service image. --->
  • <cfset local.target = imageNew( arguments.filePath ) />
  •  
  • <!--- Paste the watermark onto the target. --->
  • <cfset imagePaste(
  • local.target,
  • local.watermark,
  • (imageGetWidth( local.target ) - imageGetWidth( local.watermark )),
  • (imageGetHeight( local.target ) - imageGetHeight( local.watermark ))
  • ) />
  •  
  • <!---
  • Write the target back to file; this will override
  • the image that was produced by the ColdFusion as a
  • Service request.
  • --->
  • <cfset imageWrite(
  • local.target,
  • arguments.filePath
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, this manually created Image.cfc service component takes its arguments and passes them off to its super class (the original Image.cfc service component). Once the super class returns the resultant image URL, we take it, apply the watermark, and the return it.

Creating your own Image.cfc service component is a solid approach, and probably the one you would want to go with in the long term; but, like I said at the beginning, the whole request-intercept approach was an experiment in pushing the limits of the ColdFusion application framework. Not only was it a lot of fun to figure out, it was the first time in like 9 years that was I able to successfully "intercept" a SOAP-based request. In future versions of ColdFusion, I'd love to see more hooking into the SOAP work flow.




Reader Comments

Ben,

This article along with numerous others have provided me with so many insights into the inner guts of handling SOAP requests and responses.
We just had a developer leave suddenly who had been developing a non-CF solution, and the baton was passed to me to provide webservices to the specifications the client determined.
I ran into the classic internal server error 500 so many times I thought I wasn't going to produce viable results in time to keep the company project on track; but with your insights I got it working. I came into the same thought process as this article exemplifies, because the client is going to initiate SOAP requests, with pdf payloads, to our servers, and I needed an 'interceptor' to handle the SOAP, and to get around the various issues with gethttprequestdata(). Thank you.
Garry

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.