Saturday, 7 May 2022

Running CFML code on via a remote HTTP request


This ended up being more of a rabbit hole than I expected it to be. But in the process I've learned a bit more about curl, PHP, Python, JS (client). And actually CFML too I guess.

I can't even remember why I needed to do this, but it was something to do with testing that TinyTestFramework I've been blathering about recently.

Anyhow, I decided I needed to run some code locally on my PC which would send some code off to, run it, and send me back the response. I figured it'd be doable if I worked out what Abram was doing when I click the "Run Code" button on the UI. As it turns out it's just an HTTP post, and I could could re-run the curl captured from my browser easily enough:

Yeah I don't care

Thanks, but before you take time to mention it: I know the code blows out to the right. It doesn't matter, no-one's expecting you to read it really, and the blow-out is just just cosmetic shite.

curl '' \
  -H 'authority:' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
  -H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' \
  -H 'cache-control: max-age=0' \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryaYm5TBPgCaT5n3TK' \
  -H 'cookie: _ga=GA1.2.771352503.1647902596; _gid=GA1.2.1730098774.1651605323; _gat_gtag_UA_35934323_2=1' \
  -H 'dnt: 1' \
  -H 'origin:' \
  -H 'referer:' \
  -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: iframe' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: same-site' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36' \
  --data-raw $'------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="setupcode"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="zoom"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="code"\r\n\r\n<cfscript>\r\nwriteOutput("hi")\r\n\r\n</cfscript>\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="postcode"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="key"\r\n\r\nmain1651930152982-9b67997d-643d-3ddf-0b9f-6154282cfcf4\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="asserts"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK--\r\n' \

I can run that in bash and it works fine. BTW, the actual code I'm running is buried in the middle there, There's quite a chunk of overhead to execute that code, and I reckoned the browser was probably being a bit belt-n-braces about all the headers it was sending, and I'd not need most of them. I whittled it down to this:

curl '' \
  -H 'content-type: multipart/form-data; boundary=----__trycf__' \
  --data-raw $'------__trycf__\r\nContent-Disposition: form-data; name="setupcode"\r\n\r\n\r\n------__trycf__\r\nContent-Disposition: form-data; name="code"\r\n\r\n<cfscript>\r\nwriteOutput("hi")\r\n</cfscript>\r\n------__trycf__\r\nContent-Disposition: form-data; name="asserts"\r\n\r\n\r\n------__trycf__--\r\n' \

There's a handy site that converts curls to language-specific implementations, and CFML is one of the options: This was handy in theory, but the HTTP service call it created didn't work. Not its fault: it should have, but it seems CFHTTP can't handle that boundary syntax in the curl. Note that PHP's version also struggled, but the JS (fetch), Java and Python versions all worked fine.

This threw me for a while cos I'm not really that au fait with building HTTP requests by hand, but eventually I cracked it. Don't hand-crank the multipart boundary stuff: let CFML do it for you. So I came up with this proof of concept:

<cfset code = fileRead(expandPath("./code.cfm"))>

<cfhttp method="post" url="" result="httpResponse">
    <cfhttpparam type="formField" name="code" value=#code#>
    <cfhttpparam type="formField" name="asserts" value="">

<cfif httpResponse.statusCode EQ "200 OK">
    <cfdump var="#httpResponse#">

That's pretty simple. And my test code for this is just:

    name = "Scott Steinbeck"
    writeOutput("G'day #name#")


G'day Scott Steinbeck

(It was Scott that asked me to write this up).

That's where I've got to so far. I also want to see how I can include some set-up code. Hang on a sec whilst I watch some more HTTP traffic.


Oh OK, that was easy:

<cfset framework = fileRead(expandPath("./tinyTestFramework.cfm"))>
<cfset tests = fileRead(expandPath("./tests.cfm"))>

<cfhttp method="post" url="" result="httpResponse">
    <cfhttpparam type="formField" name="setupcode" value="#framework#">
    <cfhttpparam type="formField" name="code" value="#tests#">
    <cfhttpparam type="formField" name="asserts" value="">

<cfif httpResponse.statusCode EQ "200 OK">
    <cfdump var="#httpResponse#">

(code on github)

That example just runs the test for my test framework up on trycf instead of here on my own server. Because I can. At least now I remember why I wanted to do this in the first place, but that will be in a later article.

All these test so far only run on ColdFusion 2021, because that was what I was wanting to do. The other hosts are easy enough to glean just by watching the request when clicking "run code". The Lucee (latest) one is

Anyway, not a terribly exciting one this (not like how terribly exciting the shit I write here usually is, eh? EH??), but problem solved and hopefully this will help Scott.