CFHTTPSession.cfc For Multi-CFHttp Requests With Maintained Session

Posted March 3, 2008 at 9:29 AM

Tags: ColdFusion

There was a chance that I was going to have to write a ColdFusion script that integrated with SalesForce.com. This script would have to login into a SalesForce.com, submit some report criteria, then download an Excel report file. I have done a lot of work with ColdFusion's CFHttp and CFHttpParam tags, so I wasn't too worries about the script; however, I felt that I could come up with a way to make this kind of work easier, not just for SalesForce.com, but for session-oriented CFHttp calls in general. To do this, I created the CFHTTPSession.cfc ColdFusion component. This ColdFusion component is meant to mimic a "real" browser session by wrapping around ColdFusion CFHttp calls and taking care of all the sending and receiving of session cookies behind the scenes. This way, you just make your Get() and Post() calls though the CFHTTPSession.cfc and it will take care of maintaining your session data.

To demonstrate this API action, I am going to run a quick example that logs into Dig Deep Fitness, my iPhone fitness application, and then makes a second page request to grab the list of exercises. The list-grab will only work if the second request announces itself as being part of the same session:

 Launch code in new window » Download code as text file »

  • <!---
  • Create the CFHttpSession object that will be sued to
  • make our multiple calls to the same remote application.
  • --->
  • <cfset objHttpSession = CreateObject(
  • "component",
  • "CFHTTPSession"
  • ).Init()
  • />
  •  
  •  
  • <!---
  • Make the first call to login into Dig Deep Fitness,
  • the iPhone fitness application.
  • --->
  • <cfset objResponse = objHttpSession
  • .NewRequest( "http://www.digdeepfitness.com/index.cfm" )
  • .AddFormField( "go", "login" )
  • .AddFormField( "submitted", 1 )
  • .AddFormField( "email", "ben@XXXXXXXX.com" )
  • .AddFormField( "password", "YYYYYYYYYYYYY" )
  • .Post()
  • />
  •  
  • <!---
  • Make the second to get the list of exercises (which will
  • only be successful if the session is maintained across
  • CFHTTP calls.
  • --->
  • <cfset objResponse = objHttpSession
  • .NewRequest( "http://www.digdeepfitness.com/index.cfm" )
  • .AddUrl( "go", "exercises" )
  • .Get()
  • />
  •  
  •  
  • <!--- Output the resposne content. --->
  • <cfoutput>
  • #objResponse.FileContent#
  • </cfoutput>

As you can see, we are creating an instance of the CFHTTPSession.cfc. Then, we create a NewRequest() for the login page, and Post() the data. Then, using the same CFHTTPSession.cfc instance, we create a second request and Get() the data for the exercises list. Running the above code, we get the following response content:


 
 
 

 
CFHTTPSession.cfc Maintains Session Data Across Multiple CFHttp Calls  
 
 
 

As you can see, we have maintained the session information across multiple ColdFusion CFHttp calls and gotten the secure page data.

The API for ColdFusion component is fairly simple and can handle the most common CFHttp use-cases (I didn't bother building them all in because I simply don't use them all). Whenever you want to create a new request, you use the NewRequest() method. This takes the URL of the request and prepares the object for a new request. Then, you have the Get() method and the Post() method which just uses the different actions (GET vs. POST). Get() and Post() both return the contents of the CFHttp call.

In between those method calls, you have the chance to add data to the outgoing request parameters. This can be done through AddParam() or through the easier, utility methods:

  • AddCGI( Name, Value [, Encoded ] )
  • AddCookie( Name, Value )
  • AddFile( Name, Path [, MimeType ] )
  • AddFormField( Name, Value [, Encoded ] )
  • AddHeader( Name, Value )
  • AddUrl( Name, Value )
  • SetBody( Value )
  • SetUserAgent( Value )
  • SetXml( Value )

All of these methods return the THIS pointer to the CFHTTPSession.cfc instance so that these methods can be chained together for convenience.

The CFHTTPSession.cfc instance can be used on a single page or it can be cached in a persistent scope to be used across multiple page calls in the user's application. Of course, if the remote session times out, then the login will have to be created again - the object does not handle this for you.

I have not thoroughly tested this because, well frankly, I don't use CFHttp for many different use cases. However, much of the API relies on calling other parts of the API. As such, any bugs that pop up should be extremely easy to locate and iron out. Here is the code that is powers the CFHTTPSession.cfc ColdFusion component:

 Launch code in new window » Download code as text file »

  • <cfcomponent
  • output="false"
  • hint="Handles a CFHTTP session by sending an receving cookies behind the scenes.">
  •  
  • <!---
  • Pseudo constructor. Set up data structures and
  • default values.
  • --->
  • <cfset VARIABLES.Instance = {} />
  •  
  • <!---
  • These are the cookies that get returned from the
  • request that enable us to keep the session across
  • different CFHttp requests.
  • --->
  • <cfset VARIABLES.Instance.Cookies = {} />
  •  
  • <!---
  • The request data contains the various types of data that
  • we will send with our request. These will be both for the
  • CFHttpParam tags as well as the CFHttp property values.
  • --->
  • <cfset VARIABLES.Instance.RequestData = {} />
  • <cfset VARIABLES.Instance.RequestData.Url = "" />
  • <cfset VARIABLES.Instance.RequestData.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6" />
  • <cfset VARIABLES.Instance.RequestData.Params = [] />
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Returns an initialized component.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="UserAgent"
  • type="string"
  • required="false"
  • hint="The user agent that will be used on the subseqent page requests."
  • />
  •  
  • <!--- Check to see if we have a user agent. --->
  • <cfif StructKeyExists( ARGUMENTS, "UserAgent" )>
  • <cfset THIS.SetUserAgent( ARGUMENTS.UserAgent ) />
  • </cfif>
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddCGI"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a CGI value. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the CGI value."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="string"
  • required="true"
  • hint="The CGI value."
  • />
  •  
  • <cfargument
  • name="Encoded"
  • type="string"
  • required="false"
  • default="yes"
  • hint="Determins whether or not to encode the CGI value."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "CGI",
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value,
  • Encoded = ARGUMENTS.Encoded
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddCookie"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a cookie value. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the CGI value."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="string"
  • required="true"
  • hint="The CGI value."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "Cookie",
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddFile"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a file value. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the form field for the posted file."
  • />
  •  
  • <cfargument
  • name="Path"
  • type="string"
  • required="true"
  • hint="The expanded path to the file."
  • />
  •  
  • <cfargument
  • name="MimeType"
  • type="string"
  • required="false"
  • default="application/octet-stream"
  • hint="The mime type of the posted file. Defaults to *unknown* mime type."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "Cookie",
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddFormField"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a form value. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the form field."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="string"
  • required="true"
  • hint="The form field value."
  • />
  •  
  • <cfargument
  • name="Encoded"
  • type="string"
  • required="false"
  • default="yes"
  • hint="Determins whether or not to encode the form value."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "FormField",
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value,
  • Encoded = ARGUMENTS.Encoded
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddHeader"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a header value. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the header value."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="string"
  • required="true"
  • hint="The header value."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "Header",
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddParam"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a CFHttpParam data point. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Type"
  • type="string"
  • required="true"
  • hint="The type of data point."
  • />
  •  
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the data point."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="any"
  • required="true"
  • hint="The value of the data point."
  • />
  •  
  • <cfargument
  • name="File"
  • type="string"
  • required="false"
  • default=""
  • hint="The expanded path to be used if the data piont is a file."
  • />
  •  
  • <cfargument
  • name="MimeType"
  • type="string"
  • required="false"
  • default=""
  • hint="The mime type of the file being passed (if file is being passed)."
  • />
  •  
  • <cfargument
  • name="Encoded"
  • type="string"
  • required="false"
  • default="yes"
  • hint="The determines whether or not to encode Form Field and CGI values."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!---
  • Check to see which kind of data point we are dealing
  • with so that we can see how to create the param.
  • --->
  • <cfswitch expression="#ARGUMENTS.Type#">
  •  
  • <cfcase value="Body">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Value = ARGUMENTS.Value
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="CGI">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value,
  • Encoded = ARGUMENTS.Encoded
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="Cookie">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="File">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Name = ARGUMENTS.Name,
  • File = ARGUMENTS.File,
  • MimeType = ARGUMENTS.MimeType
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="FormField">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value,
  • Encoded = ARGUMENTS.Encoded
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="Header">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="Url">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • } />
  •  
  • </cfcase>
  •  
  • <cfcase value="Xml">
  •  
  • <!--- Create the param. --->
  • <cfset LOCAL.Param = {
  • Type = ARGUMENTS.Type,
  • Value = ARGUMENTS.Value
  • } />
  •  
  • </cfcase>
  •  
  • </cfswitch>
  •  
  •  
  • <!--- Add the parameter for the next request. --->
  • <cfset ArrayAppend(
  • VARIABLES.Instance.RequestData.Params,
  • LOCAL.Param
  • ) />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddUrl"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Adds a url value. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="The name of the header value."
  • />
  •  
  • <cfargument
  • name="Value"
  • type="string"
  • required="true"
  • hint="The header value."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "Url",
  • Name = ARGUMENTS.Name,
  • Value = ARGUMENTS.Value
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Get"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="Uses the GET method to place the next request. Returns the CFHttp response.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="GetAsBinary"
  • type="string"
  • required="false"
  • default="auto"
  • hint="Determines how to return the file content - return as binary value."
  • />
  •  
  • <!--- Return response. --->
  • <cfreturn THIS.Request(
  • Method = "get",
  • GetAsBinary = ARGUMENTS.GetAsBinary
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GetCookies"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="Returns the internal session cookies.">
  •  
  • <cfreturn VARIABLES.Instance.Cookies />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="NewRequest"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Sets up the object for a new request. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Url"
  • type="string"
  • required="true"
  • hint="The URL for the new request."
  • />
  •  
  • <!--- Store the passed-in url. --->
  • <cfset VARIABLES.Instance.RequestData.Url = ARGUMENTS.Url />
  •  
  • <!--- Clear the request data. --->
  • <cfset VARIABLES.Instance.RequestData.Params = [] />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Post"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="Uses the POST method to place the next request. Returns the CFHttp response.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="GetAsBinary"
  • type="string"
  • required="false"
  • default="auto"
  • hint="Determines how to return the file content - return as binary value."
  • />
  •  
  • <!--- Return response. --->
  • <cfreturn THIS.Request(
  • Method = "post",
  • GetAsBinary = ARGUMENTS.GetAsBinary
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Request"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="Performs the CFHttp request and returns the response.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Method"
  • type="string"
  • required="false"
  • default="get"
  • hint="The type of request to make."
  • />
  •  
  • <cfargument
  • name="GetAsBinary"
  • type="string"
  • required="false"
  • default="auto"
  • hint="Determines how to return body."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!---
  • Make request. When the request comes back, we don't
  • want to follow any redirects. We want this to be
  • done manually.
  • --->
  • <cfhttp
  • url="#VARIABLES.Instance.RequestData.Url#"
  • method="#ARGUMENTS.Method#"
  • useragent="#VARIABLES.Instance.RequestData.UserAgent#"
  • getasbinary="#ARGUMENTS.GetAsBinary#"
  • redirect="no"
  • result="LOCAL.Get">
  •  
  • <!---
  • In order to maintain the user's session, we are
  • going to resend any cookies that we have stored
  • internally.
  • --->
  • <cfloop
  • item="LOCAL.Key"
  • collection="#VARIABLES.Instance.Cookies#">
  •  
  • <cfhttpparam
  • type="cookie"
  • name="#LOCAL.Key#"
  • value="#VARIABLES.Instance.Cookies[ LOCAL.Key ].Value#"
  • />
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • At this point, we have done everything that we
  • need to in order to maintain the user's session
  • across CFHttp requests. Now we can go ahead and
  • pass along any addional data that has been specified.
  • --->
  •  
  •  
  • <!--- Loop over params. --->
  • <cfloop
  • index="LOCAL.Param"
  • array="#VARIABLES.Instance.RequestData.Params#">
  •  
  • <!---
  • Pass the existing param object in as our
  • attributes collection.
  • --->
  • <cfhttpparam
  • attributecollection="#LOCAL.Param#"
  • />
  •  
  • </cfloop>
  •  
  • </cfhttp>
  •  
  •  
  • <!---
  • Store the response cookies into our internal cookie
  • storage struct.
  • --->
  • <cfset StoreResponseCookies( LOCAL.Get ) />
  •  
  •  
  • <!---
  • Check to see if there was some sort of redirect
  • returned with the repsonse. If there was, we want
  • to redirect with the proper value.
  • --->
  • <cfif StructKeyExists( LOCAL.Get.ResponseHeader, "Location" )>
  •  
  • <!---
  • There was a response, so now we want to do a
  • recursive call to return the next page. When
  • we do this, make sure we have the proper URL
  • going out.
  • --->
  • <cfif REFindNoCase(
  • "^http",
  • LOCAL.Get.ResponseHeader.Location
  • )>
  •  
  • <!--- Proper url. --->
  • <cfreturn THIS
  • .NewRequest( LOCAL.Get.ResponseHeader.Location )
  • .Get()
  • />
  •  
  • <cfelse>
  •  
  • <!---
  • Non-root url. We need to append the current
  • redirect url to our last URL for relative
  • path traversal.
  • --->
  • <cfreturn THIS
  • .NewRequest(
  • GetDirectoryFromPath( VARIABLES.Instance.RequestData.Url ) &
  • LOCAL.Get.ResponseHeader.Location
  • )
  • .Get()
  • />
  •  
  • </cfif>
  •  
  • <cfelse>
  •  
  • <!---
  • No redirect, so just return the current
  • request response object.
  • --->
  • <cfreturn LOCAL.Get />
  •  
  • </cfif>
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="SetBody"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Sets the body data of next request. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Value"
  • type="any"
  • required="false"
  • hint="The data body."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "Body",
  • Name = "",
  • Value = ARGUMENTS.Value
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="SetUserAgent"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Sets the user agent for next request. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Value"
  • type="string"
  • required="false"
  • hint="The user agent that will be used on the subseqent page requests."
  • />
  •  
  • <!--- Store value. --->
  • <cfset VARIABLES.Instance.RequestData.UserAgent = ARGUMENTS.UserAgent />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="SetXml"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Sets the XML body data of next request. Returns THIS scope for method chaining.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Value"
  • type="any"
  • required="false"
  • hint="The data body."
  • />
  •  
  • <!--- Set parameter and return This reference. --->
  • <cfreturn THIS.AddParam(
  • Type = "Xml",
  • Name = "",
  • Value = ARGUMENTS.Value
  • ) />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="StoreResponseCookies"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="This parses the response of a CFHttp call and puts the cookies into a struct.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Response"
  • type="struct"
  • required="true"
  • hint="The response of a CFHttp call."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!---
  • Create the default struct in which we will hold
  • the response cookies. This struct will contain structs
  • and will be keyed on the name of the cookie to be set.
  • --->
  • <cfset LOCAL.Cookies = StructNew() />
  •  
  • <!---
  • Get a reference to the cookies that werew returned
  • from the page request. This will give us an numericly
  • indexed struct of cookie strings (which we will have
  • to parse out for values). BUT, check to make sure
  • that cookies were even sent in the response. If they
  • were not, then there is not work to be done.
  • --->
  • <cfif NOT StructKeyExists(
  • ARGUMENTS.Response.ResponseHeader,
  • "Set-Cookie"
  • )>
  •  
  • <!--- No cookies were send back so just return. --->
  • <cfreturn />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: We know that cookie were returned in the page
  • response and that they are available at the key,
  • "Set-Cookie" of the reponse header.
  • --->
  •  
  •  
  • <!---
  • The cookies might be coming back as a struct or they
  • might be coming back as a string. If there is only
  • ONE cookie being retunred, then it comes back as a
  • string. If that is the case, then re-store it as a
  • struct.
  • --->
  • <cfif IsSimpleValue( ARGUMENTS.Response.ResponseHeader[ "Set-Cookie" ] )>
  •  
  • <cfset LOCAL.ReturnedCookies = {} />
  • <cfset LOCAL.ReturnedCookies[ 1 ] = ARGUMENTS.Response.ResponseHeader[ "Set-Cookie" ] />
  •  
  • <cfelse>
  •  
  • <!--- Get a reference to the cookies struct. --->
  • <cfset LOCAL.ReturnedCookies = ARGUMENTS.Response.ResponseHeader[ "Set-Cookie" ] />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • At this point, we know that no matter how the
  • cookies came back, we have the cookies in a
  • structure of cookie values.
  • --->
  • <cfloop
  • item="LOCAL.CookieIndex"
  • collection="#LOCAL.ReturnedCookies#">
  •  
  • <!---
  • As we loop through the cookie struct, get
  • the cookie string we want to parse.
  • --->
  • <cfset LOCAL.CookieString = LOCAL.ReturnedCookies[ LOCAL.CookieIndex ] />
  •  
  •  
  • <!---
  • For each of these cookie strings, we are going
  • to need to parse out the values. We can treate
  • the cookie string as a semi-colon delimited list.
  • --->
  • <cfloop
  • index="LOCAL.Index"
  • from="1"
  • to="#ListLen( LOCAL.CookieString, ';' )#"
  • step="1">
  •  
  • <!--- Get the name-value pair. --->
  • <cfset LOCAL.Pair = ListGetAt(
  • LOCAL.CookieString,
  • LOCAL.Index,
  • ";"
  • ) />
  •  
  • <!---
  • Get the name as the first part of the pair
  • sepparated by the equals sign.
  • --->
  • <cfset LOCAL.Name = ListFirst( LOCAL.Pair, "=" ) />
  •  
  • <!---
  • Check to see if we have a value part. Not all
  • cookies are going to send values of length,
  • which can throw off ColdFusion.
  • --->
  • <cfif (ListLen( LOCAL.Pair, "=" ) GT 1)>
  •  
  • <!--- Grab the rest of the list. --->
  • <cfset LOCAL.Value = ListRest( LOCAL.Pair, "=" ) />
  •  
  • <cfelse>
  •  
  • <!---
  • Since ColdFusion did not find more than
  • one value in the list, just get the empty
  • string as the value.
  • --->
  • <cfset LOCAL.Value = "" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have the name-value data values,
  • we have to store them in the struct. If we
  • are looking at the first part of the cookie
  • string, this is going to be the name of the
  • cookie and it's struct index.
  • --->
  • <cfif (LOCAL.Index EQ 1)>
  •  
  • <!---
  • Create a new struct with this cookie's name
  • as the key in the return cookie struct.
  • --->
  • <cfset LOCAL.Cookies[ LOCAL.Name ] = StructNew() />
  •  
  • <!---
  • Now that we have the struct in place, lets
  • get a reference to it so that we can refer
  • to it in subseqent loops.
  • --->
  • <cfset LOCAL.Cookie = LOCAL.Cookies[ LOCAL.Name ] />
  •  
  •  
  • <!--- Store the value of this cookie. --->
  • <cfset LOCAL.Cookie.Value = LOCAL.Value />
  •  
  •  
  • <!---
  • Now, this cookie might have more than just
  • the first name-value pair. Let's create an
  • additional attributes struct to hold those
  • values.
  • --->
  • <cfset LOCAL.Cookie.Attributes = StructNew() />
  •  
  • <cfelse>
  •  
  • <!---
  • For all subseqent calls, just store the
  • name-value pair into the established
  • cookie's attributes strcut.
  • --->
  • <cfset LOCAL.Cookie.Attributes[ LOCAL.Name ] = LOCAL.Value />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • Now that we have all the response cookies in a
  • struct, let's append those cookies to our internal
  • response cookies.
  • --->
  • <cfset StructAppend(
  • VARIABLES.Instance.Cookies,
  • LOCAL.Cookies
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

I am looking forward to possibly putting this to the test with a ColdFusion / SalesForce.com integration, but really, this should work with any kind of cookie-based session application.

Download Code Snippet ZIP File

Comments (41)  |  Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page




I'm Too Young For This!

Reader Comments

dude, badass! I can see great potential for this kind of thing in programmatic, automated testing, too. Particularly "smoke test" kind of tests where you just want your tests to run through the site and make sure you don't get any 400/500 errors.

Posted by marc esher on Mar 3, 2008 at 9:44 AM


You should riaforge this!

Posted by Raymond Camden on Mar 3, 2008 at 12:33 PM


Hey Ben

This is cool - is there a specific reason for using Firefox as the user agent?

Dom

Posted by DrDom on Mar 3, 2008 at 4:14 PM


Does this cfc handle cookies based on domain? I have a login that goes through several redirects between different servers, and through all the redirects, I only need to send the cookies based on the current domain. I have written something like this, but just a recursive function. It is not as clean as yours. Also does this handle the SSL certificates if I import them in the keystore?

Posted by Matthew on Mar 3, 2008 at 7:49 PM


Thanks, great post and source. This gives me a great example of coding, cause I´m still learning. Is there a special reason why to use Firefox as client?

Posted by Werbeagentur on Mar 5, 2008 at 4:11 AM


@DrDom,

I use FireFox cause when I make a web call, I like to announce myself as the most awesome browser in town :)

@Matthew,

I never considered changing domains. I guess, this would just keep accumulating cookies and then send them across no matter what the next domain is. Since it manually follows the Location redirects sent back by CFLocation type tags, I think it will keep sending cookies.

Do you think I should make the cookies domain based? I could keep all the information keyed in a structure that is domain specific.

@Ray,

When I get some time, I will put it up.

Posted by Ben Nadel on Mar 6, 2008 at 8:47 AM


No you dont have to worry about domain cookies. For my situation i just needed to hold onto the last set of cookies for that last redirect that was made.

I just dont want to send unecessary cookies. Once all the redirects happened, i just need the cookies from that final redirect.

Good job ben.

-Matthew

Posted by Matthew on Mar 6, 2008 at 11:41 AM


@Matthew,

Sounds good. If you see anywhere that this can be improved, let me know.

Posted by Ben Nadel on Mar 6, 2008 at 11:47 AM


I was trying to write something like this the other day... couldn't figure it out and gave up. But THIS IS TOO COOL. Thanks for the lesson. Can't wait to try it out.

Posted by Alan Johnson on Mar 10, 2008 at 11:29 PM


@Alan,

Glad you are excited about this. Let me know if you see any ways that it can be improved.

Posted by Ben Nadel on Mar 11, 2008 at 7:20 AM


Coldfusion Server complained about the invalid token '{' in CFHTTPSession.cfc. Is it because we are running CFMX 6.1?

Posted by Joshua Shaffner on Mar 12, 2008 at 4:24 PM


@Joshua,

Yeah, that's a version issue. This is ColdFusion 8 compatible code. The {} notation is an implicit struct. YOu can try to replace things like:

<cfset var LOCAL = {} />

... with:

<cfset var LOCAL = StructNew() />

There might be some other areas that are not compatible as well, but I believe that all of this should be able to be converted in ColdFusion MX 6 compatible.

Unfortunately, I don't have access to an MX6 machine and cannot test any of the code.

Posted by Ben Nadel on Mar 12, 2008 at 4:40 PM


Okay. What about []? Put in ArrayNew(1)?

After I did just that, I am now looking at this piece of code where attribute, "array" is not supported.

<cfloop index="LOCAL.Param" array="#VARIABLES.Instance.RequestData.Params#">

Posted by Joshua Shaffner on Mar 12, 2008 at 5:26 PM


@Joshua,

<cfloop index="LOCAL.Param" array="#VARIABLES.Instance.RequestData.Params#">

Becomes:

<cfloop
index="LOCAL.ParamIndex"
from="1"
to="#ArrayLen( VARIABLES.Instance.RequestData.Params )#"
step="1">

<!--- Get short hand to next param. --->
<cfset LOCAL.Param = VARIABLES.Instance.RequestData.Params[ LOCAL.ParamIndex] />

Posted by Ben Nadel on Mar 12, 2008 at 5:29 PM


@Ben

I am now looking at the error where cfhttpparam's attribute, "attributecollection" is not supported.

Note: It has been few years since I last worked with ColdFusion. Much appreciated for your help!

Posted by Joshua Shaffner on Mar 12, 2008 at 5:36 PM


@Joshua,

Oooh :( That's a bit of a tougher one! You have to take a little bit more code here to actually get that to work. Instead of just passing in the attribute collection, you are gonna have to define the individual CFHttpParam tags.

For example:

<cfcase value="Header">

<cfhttpparam
type="header"
name="#LOCAL.Param.Name#"
value="#LOCAL.Param.Value#"
/>

</cfcase>

<cfcase value="CGI">

<cfhttpparam
type="cgi"
name="#LOCAL.Param.Name#"
value="#LOCAL.Param.Value#"
encode="#LOCAL.Param.Encode#"
/>

</cfcase>

Basically, you have to enumerate each CFCase tag rather than just using the one tag.

Posted by Ben Nadel on Mar 12, 2008 at 5:51 PM


@Ben

After putting in a bunch of code replacing a single line of attribute collection. I no longer get any compile error from CFHTTPSession.cfc.

I am now figuring out why I would get different JSESSIONID on every request against java/jsp off a tomcat. Am I supposed to see same JSESSION on every request?

Thanks.

Posted by Joshua Shaffner on Mar 13, 2008 at 9:12 AM


@Joshua,

I don't know too much about jsessionID. I think you should keep it from page to page. Hmmm :( That's awesome that you got it to compile, though. Very well done.

Posted by Ben Nadel on Mar 13, 2008 at 9:26 AM


@Joshua,

I have another person who is having trouble with maintaining session across pages. I am gonna try to debug that at lunch; I might find some good stuff. I will let you know.

Posted by Ben Nadel on Mar 13, 2008 at 9:27 AM


Very good article.

Posted by Fredy on Mar 13, 2008 at 10:13 AM


When you call a cfhttp it will always open up a new request, unless you store the cookie info (jsession) in a session, or some kind of persistant state.

When i do these types of things with Cfhttp, i hold on to the object in a session so the cookies persist.

Hope that makes sense.

-Matthew

Po