ColdFusion CFEXECUTE

 Mikes Notes

  • I am learning to use <cfexecute> to enable Pipi to use the command line to control its host server autonomously.
  • The server has no public access.
  • How to do this safely?
  • Code examples below in yellow

See notes from

  • Adobe Help
  • CFDocs
  • Ben Nadel (2020)
  • Brian Harvey - Heartland Web Development (2015)

From Adobe Help > cfexecute

Executes a ColdFusion developer-specified process on a server computer.

<cfexecute
name = "application name"
arguments = "command line arguments"
outputFile = "output filename"
errorFile = "filename to store error output"
timeout = "timeout interval"
variable = "variable name"
errorVariable = "variable name">
...
</cfexecute>

From CFDocs > cfexecute

name (string)

Absolute path of the application to execute.


On Windows, you must specify an extension; for example, C:\myapp.exe.

arguments (any)

Command-line variables passed to application. If specified as string, it is processed as follows:

  • Windows: passed to process control subsystem for parsing.
  • UNIX: tokenized into an array of arguments. The default token separator is a space; you can delimit arguments that have embedded spaces with double quotation marks.
If passed as array, it is processed as follows:
  • Windows: elements are concatenated into a string of tokens, separated by spaces. Passed to process control subsystem for parsing.
  • UNIX: elements are copied into an array of exec() arguments

outputfile (string)

File to which to direct program output. If no outputfile or variable attribute is specified, output is displayed on the page from which it was called.

If not an absolute path (starting with a drive letter and a colon, or a forward or  backward slash), it is relative to the CFML temporary directory, which is returned 
by the GetTempDirectory function.

variable (string)

Variable in which to put program output. If no outputfile or variable attribute is specified, output is displayed on page from which it was called.

timeout (numeric)

Default: 0
Length of time, in seconds, that CFML waits for output from the spawned program.

errorVariable (string)

The name of a variable in which to save the error stream output.

errorFile (string)

The pathname of a file in which to save the error stream output. If not an absolute path (starting a with a drive letter and a colon, or a forward or backward slash), it is relative to the ColdFusion temporary directory, which is returned by the GetTempDirectory function.

terminateOnTimeout (boolean

Default: false
Lucee 4.5+ Terminate the process after the specified timeout is reached. Ignored if timeout is not set or is 0.

directory (string)

Lucee 5.3.8+ The working directory in which to execute the command

From Ben Nadel's Blog

Running CFExecute From A Given Working Directory In Lucee CFML 5.2.9.31
5 April 2020

When you invoke the CFExecute tag in ColdFusion, there is no option to execute the given command from a particular working directory. That's why I recently looked at using the ProcessBuilder class to execute commands in ColdFusion. That said, the default community response to anyone who runs into a limitation with the CFExecute tag is generally, "Put your logic in a bash-script and then execute the bash-script.". I don't really know anything about bash scripting (the syntax looks utterly cryptic); so, I thought, it might be fun to try and create a bash script that will proxy arbitrary commands in order to execute them in a given working directory in Lucee CFML 5.2.9.31.

The goal here is to create a bash script that will take N-arguments in which the 1st argument is the working directory from which to evaluate the rest (2...N) of the arguments. So, instead of running something like:

ls -al path/to/things

... we could run something like:

my-proxy path/to ls -al things

In this case, we'd be telling the my-proxy command to execute ls -al things from within the path/to working directory. To hard-code this example as a bash script, we could write something like this:

cd path/to # Change working directory.

ls -al ./things # Run ls command RELATIVE to WORKING DIRECTORY.

The hard-coded version illustrates what we're trying to do; but, we want this concept to by dynamic such that we could run any command from any directory. To this end, I've created the following bash script, execute_from_directory.sh, through much trial and error:

#!/bin/sh

# In the current script invocation, the first argument needs to be the WORKING DIRECTORY
# from whence the rest of the script will be executed.
working_directory=$1

# Now that we have the working directory argument saved, SHIFT IT OFF the arguments list.
# This will leave us with a "$@" array that contains the REST of the arguments.
shift

# Move to the target working directory.
cd "$working_directory"

# Execute the REST of command from within the new working directory.
# --
# NOTE: The $@ is a special array in BASH that contains the input arguments used to
# invoke the current executable.
"$@"

CAUTION: Again, I have no experience with bash script. As such, please take this exploration with a grain of salt - a point of inspiration, not a source of truth!

What this is saying, as best as I think I understand it, is take the first argument as the desired working directory. Then, use the shift command to shift all the other arguments over one (essentially shifting the first item off of the arguments array). Then, change working directory and execute the rest of the arguments from within the new working directory.

Because we are using the special arguments array notation, "$@", instead of hard-coding anything, we should be able to pass-in an arbitrary set of arguments. I think. Again, I have next to no experience here.

Once I created this bash-script, I had to change the permissions to allow for execution:

chmod +x execute_from_directory.sh

To test this from the command-line, outside of ColdFusion, I tried to list out the files in my images directory - path/to/my-cool-images - using a combination of working directories and relative paths:

wwwroot# ./execute_from_directory.sh path/to ls -al ./my-cool-images

total 17740
drwxr-xr-x 11 root root     352 Apr 11 11:28 .
drwxr-xr-x  4 root root     128 Apr 15 10:07 ..
-rw-r--r--  1 root root 1628611 Dec  1 20:04 broad-city-yas-queen.gif
-rw-r--r--  1 root root  188287 Mar  4 14:09 cfml-state-of-the-union.jpg
-rw-------  1 root root 3469447 Jan 28 16:59 dramatic-goose.gif
-rw-------  1 root root 2991674 Dec 14 15:39 engineering-mistakes.gif
-rw-r--r--  1 root root  531285 Dec  1 21:10 monolith-to-microservices.jpg
-rw-r--r--  1 root root  243006 Dec 24 12:34 phoenix-project.jpg
-rw-r--r--  1 root root 1065244 Jan 22 14:41 rob-lowe-literally.gif
-rw-r--r--  1 root root 7482444 Mar 25 10:15 thanos-inifinity-stones.gif
-rw-r--r--  1 root root  239090 Dec 29 13:08 unicorn-project.jpg

As you can see, I was able to execute the ls command from within the path/to working directory! Woot woot!

To test this from ColdFusion, I'm going to recreate my zip storage experiment; but, instead of using the ProcessBuilder class, I'm going to use the CFExecute tag to run the zip command through my execute_from_directory.sh bash script:

<cfscript>
// Reset demo on subsequent executions.
cleanupFile( "./images.zip" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Normally, CFExecute has no sense of a "working directory" during execution.
// However, by proxying our command-line execution through a Shell Script (.sh), we
// can CD (change directory) to a given directory and then dynamically execute the
// rest of the commands.
executeFromDirectory(
// This is the WORKING DIRECTORY that will become the context for the rest of
// the script execution.
expandPath( "./path/to" ),
// This is the command that we are going to execute from the WORKING DIRECTORY.
// In this case, we will execute the ZIP command using RELATIVE PATHS that are
// relative to the above WORKING DIRECTORY.
"zip",
// These are the arguments to pass to the ZIP command.
[
// Regulate the speed of compression: 0 means NO compression. This is setting
// the compression method to STORE, as opposed to DEFLATE, which is the
// default method. This will apply to all files within the zip - if we wanted
// to target only a subset of file-types, we could have used "-n" to white-
// list a subset of the input files (ex, "-n .gif:.jpg:.jpeg:.png").
"-0",
// Recurse the input directory.
"-r",
// Define the OUTPUT file (our generated ZIP file).
expandPath( "./images.zip" ),
// Define the INPUT file - NOTE that this path is RELATIVE TO THE WORKING
// DIRECTORY! By using a relative directory, it allows us to generate a ZIP
// in which the relative paths become the entries in the resultant archive.
"./my-cool-images",
// Don't include files in zip.
"-x *.DS_Store"
]
);
echo( "<br />" );
echo( "Zip file size: " );
echo( numberFormat( getFileInfo( "./images.zip" ).size ) & " bytes" );
echo( "<br /><br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I execute the given series of commands from the given working directory. The
* standard output is printed to the page. If an error is returned, the page request
* is aborted.
* @workingDirectory I am the working directory from whence to execute commands.
* @commandName I am the command to execute from the working directory.
* @commandArguments I am the arguments for the command.
*/
public void function executeFromDirectory(
required string workingDirectory,
required string commandName,
required array commandArguments
) {
// The Shell Script that's going to proxy the commands is expecting the working
// directory to be the first argument. As such, let's create a normalized set of
// arguments for our proxy that contains the working directory first, followed by
// the rest of the commands.
var normalizedArguments = [ workingDirectory ]
.append( commandName )
.append( commandArguments, true )
;
execute
name = expandPath( "./execute_from_directory.sh" ),
arguments = normalizedArguments.toList( " " )
variable = "local.successOutput"
errorVariable = "local.errorOutput"
timeout = 10
terminateOnTimeout = true
;
if ( len( errorOutput ?: "" ) ) {
dump( errorOutput );
abort;
}
echo( "<pre>" & ( successOutput ?: "" ) & "</pre>" );
}
/**
* I delete the given file if it exists.
* @filename I am the file being deleted.
*/
public void function cleanupFile( required string filename ) {
if ( fileExists( filename ) ) {
fileDelete( filename );
}
}
</cfscript>


As you can see, I've created an executeFromDirectory() User-Defined Function (UDF) which takes, as its first argument, the working directory from which we are going to execute the rest of the commands. Then, instead of executing the zip command directly, we are proxying it through our bash script.

And, when we run the above ColdFusion code, we get the following output:



Very cool! It worked! As you can see from the zip debug output, the entries in the archive are based on the relative paths from the working directory that we passed to our proxy.


Now that I know that the ProcessBuilder class exists, I'll probably just go with that approach in the future. That said, it was exciting (and, honestly, very frustrating) for me to write my first real bash-script to allow the CFExecute tag to execute commands from a given working directory in Lucee CFML. Bash scripting seems.... crazy; but, it also seems something worth learning a bit more about.

You Might Also Enjoy Some of My Other Posts

From Heartland Web Development

By Brian Harvey 27 April 2015

In order to create a real time dynamic IP whitelist solution for a client I needed to be able to SSH into a pfSense fiewall using ColdFusion and kick off a few .sh files to update the firewall's ip whitelist. ColdFusion doesn't have the ability to SSH directly, but by using <cfexecute>, Putty and Plink you can get the job done.

Here is how to do it:

1.  Download Putty and Plink. 
Putty is an SSH client for windows, and Plink is a command line interface to Putty.

2. Launch Putty and create a "stored session" to the target server. I named my stored session "firewall".  Now log into the remote server using the saved session so that an authentication key is generated and stored in Putty. Once you have generated an authentication key and are logged in you can exit your session and close Putty.



3. Now you can run <cfexecute> to SSH into the remote server and run .sh files.


<cfexecute name="C:\WINDOWS\system32\cmd.exe"

      arguments="/c C:\plink.exe -v root@firewall -pw MyPassword /cf/conf/putconfig.sh"  timeout="5">

</cfexecute>

There was one "gotcha" I discovered with running the command using ColdFusion.  I was able to run the plink command all day long from the cmd prompt:

C:\plink.exe -v root@firewall -pw MyPassword /cf/conf/putconfig.sh.

But when I tried to run it as an argument in <cfexecute> it would fail.  I was stumped until I came across this blog post by Ben Forta.

Ben points out that in Windows, you need to insert "/c" as the first argument in the string in order to tell Windows to to spin up a command interpreter to run and terminate upon completion. 

This Works:  arguments="/c C:\plink.exe -v root@firewall -pw MyPassword /cf/conf/putconfig.sh"  timeout="5"

This Doesn't Work:  arguments="C:\plink.exe -v root@firewall -pw MyPassword /cf/conf/putconfig.sh"  timeout="5"

That little extra had me spinning my wheels for the better part of a day until I ran across Ben's post.

No comments:

Post a Comment