Tuesday 4 May 2021

Short version: getting CFWheels working outside the context of a web-browsable directory

G'day:

This is all extracted from my earlier article "Installing and running CFWheels in my Lucee Docker container" from a few days ago. Why am I repeating it? Because it kinda got lost in the morass of my other witterings, but it's probably reasonable information to have as a stand-alone guide.

This is also a living document. The steps here work for the bits of CFWheels I've used so far, but perhaps need tweaking as I delve deeper into it. I'll keep it up to date wth my findings. If you spot anything additional I need to do, let me know. I suspect there might be some config settings in the last section that might need adjustment. The code that uses those settings is pretty impenetrable, so I figured I'd discover by experimentation, not wading through the CFWheels codebase.

This still looks like a chunk of instructions, but it boils down to this:

  1. point your web root at the correct directory on both the CFML server and web server;
  2. get the CFWheels code;
  3. move it around a bit;
  4. do some source control;
  5. add some server mappings;
  6. add some application mappings;
  7. change some CFWheels config.

It's easy.

What?

CFWheels official installation guidance is to put all its files in a web browsable directory. I would never generally install a web application like this, and nor should you.

Why?

There are, conventionally, three components to the code in a web app:

  • Elements that need to be web browsable. Primarily images, JS, CSS and other assets the web server needs to serve to present the website. And secondary to that, the entry point to the web application. In the case of a CFML app this is a stub index.cfm and Application.cfc (both of which point back to the application code, and don't do much else other than that).
  • The web application. This is your CFML code. The stuff in source control, and the the stuff all your tests hit.
  • Third-party code. Stuff like the frameworks you use, and other libraries you might need along the way. You simply use this code, you do not maintain it.
  • OK, so a possible fourth: tests. These are usually separate from the application source code.

These are three different things, and they belong in three different places, for both security and management reasons. They should not be munged together, and it's not a good approach to suggest that they ought to be.

Where?

There's no standard in the CFML world that I am aware of, so in this case I am borrowing from how composer handles it in PHP applications. This puts third party code in a vendor directory, categorised by the vendor name within that (and the specific project from that vendor within that). I have this (very summarised):

/var/www/
├── public/
│  ├── images/
│  ├── javascripts/
│  ├── stylesheets/
│  ├── Application.cfc
│  └── index.cfm
├── src/
│  ├── controllers/
│  ├── models/
│  ├── views/
│  └── Application.cfc
├── test/
│  ├── functional/
│  ├── integration/
│  ├── unit/
│  └── Application.cfc
└── vendor/
  ├── cfwheels/
  └── testbox/
  • public is the web site's webroot. The other directories listed here are not web accessible. In fact they're not even present on the web server.
  • I did not see the need to have the vendor-company directory within vendor, so have just gone straight to the app level with cfwheels and testbox
  • It doesn't matter where you put the third-party stuff, as long as it's not public, and not muddled in with your own code. You could use org/cfwheels/cfwheels and com/ortussolutions/testbox if you wanted to. Or some other sensible schema.

How?

Step 1: ensure only the public directory is web browsable

I'm using Lucee via their official Docker container here, but the reconfiguration requirements (if not exact implementation) will apply on ColdFusion too, as well as Lucee installed via other mechanisms.

The general configuration of a CFML server is to create some directory (in my case /var/www), and say "this is the directory that's web browsable, stick you code in there". This is bad default advice, and encourages bad and insecure practice as that default. We only want the public directory of our app to be web browsable; not the whole codebase! Fortunately thisis reasonably easily remedied. The web-browsable directory is set in $TOMCAT_HOME/config/server.xml. For me this is /usr/local/tomcat/conf/server.xml. In that file there is this section:

<Host name="127.0.0.1"  appBase="webapps" unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.RemoteIpValve"
         remoteIpHeader="X-Forwarded-For"
         requestAttributesEnabled="true" />
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
         prefix="localhost_access_log" suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b" />

    <Context path="" docBase="/var/www">
        <JarScanner scanClassPath="false"/>
    </Context>
</Host>

Change the /var/www to /var/www/public.

I've handled this for my Docker container by grabbing the file, saving it in my build context, and copying it across when I'm building the container (docker/lucee/Dockerfile):

COPY ./server.xml /usr/local/tomcat/conf/server.xml

Configuring your web server is outwith the remit of this article, but your website's doc root should be the public directory too, obviously.

This is also not related to this exercise, but whilst we're mentioning web servers, also follow the CFWheels docs when it comes to configuring rewrites from the CFWheels end of things. But I basically needed to add this to the bottom of config/settings.cfm:

set(URLRewriting="On")
set(rewriteFile="index.cfm")
Step 2: getting the CFWheels code

I used CommandBox to install CFWheels from Forgebox. This is my box.json:

{
    "dependencies":{
        "cfwheels":"^2.2.0"
    },
    "devDependencies":{
        "testbox":"^4.2.1+400"
    },
    "installPaths":{
        "testbox":"vendor/testbox/",
        "cfwheels":"vendor/cfwheels/"
    },
    "testbox":{
        "runner":"http://localhost:8888/test/runTests.cfm"
    }
}
Step 3: separating the CFWheels application implementation stubs from the framework application

All the CFWheels code is now in vendor/cfwheels. Some of this code is the basis for our application which we need to change; some of it is the CFWheels framework code. We need to copy some files to different more appropriate locations. All source file references below are relative to vendor/cfwheels. Destination locations are relative to the /var/www/ directory shown above.

  • index.cfm and rewrite.cfm need to be copied to public/
  • Application.cfc contains only an include for /wheels/functions.cfm. Copy only this include statement to be the last statement of src/Application.cfc.
  • tests/ should be copied to /var/www (adjacent to src/). These need to be source controlled, as they will contain your test code.
  • All the other subdirectories other than wheels/ of vendor/cfwheels should be copied to src/. These need to be source controlled, as they will contain your application code.
Step 4: omit src/wheels from source control
# .gitignore
/src/wheels/
Step 5: copy vendor/cfwheels/wheels to src/ during deployment

I'm using Docker for this, but as some separate step when deploying your code, you need to put the un-source-controlled vendor/cfwheels/wheels into src/. This is clunky and this directory does not belong in the middle of your application code, but there are a couple of hard-coded touch points to your source code within the wheels codebase, and try as I might, I could not work out how to override them.

WORKDIR  /var/www

RUN git clone git@github.com:adamcameron/cfml-in-docker.git .

RUN mkdir -p vendor
RUN box install

RUN cp -R /var/www/vendor/cfwheels/wheels /var/www/src/wheels
Step 6: Server mappings

Add some mappings that need to be at server level (not in Application.cfc), because Application.cfc files need them internally. I have done this with CFConfig, but you could do it via the admin UI:

{
    "CFMappings":{
        "/cfmlInDocker":{
            "inspectTemplate":"once",
            "physical":"/var/www/src",
            "primary":"physical"
        },
        "/test":{
            "inspectTemplate":"once",
            "physical":"/var/www/test",
            "primary":"physical"
        },
        "/wheels":{
            "inspectTemplate":"once",
            "physical":"/var/www/src/wheels",
            "primary":"physical"
        }
    }
}

My application's code is referenced with the mapping prefix /cfmlInDocker.

public/Application.cfc needs to extend cfmlInDocker.Application (ie: the one in the src/ directory). It can/should be otherwise empty.

We need the /wheels mapping to accommodate an include to a file on that path that we needed to put in src/Application.cfc.

TestBox's test runners all work via HTTP requests, so unfortuately the /test directory needs to be web accessible (these mappings are for code location, and also make the resources web-accessible on the internal CFML web server). They should not be exposed on your actual web server, so make sure they are blocked there.

Step 7: add application mappings to src/Application.cfc

The tail of your src/Application.cfc should end like this:

    thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())

    this.mappings["/public/wheels"] =  getCanonicalPath("#thisDirectory#wheels")
    this.mappings["/app/wheels"] = getCanonicalPath("#thisDirectory#wheels")

    this.mappings["/app/controllers"] = getCanonicalPath("#thisDirectory#controllers")
    this.mappings["/app/models"] = getCanonicalPath("#thisDirectory#models")
    this.mappings["/app/events"] = getCanonicalPath("#thisDirectory#events")
    this.mappings["/app/files"] = getCanonicalPath("#thisDirectory#files")
    this.mappings["/app/plugins"] = getCanonicalPath("#thisDirectory#plugins")
    this.mappings["/app/views"] = getCanonicalPath("#thisDirectory#views")

    testDirectory = getCanonicalPath("#thisDirectory#../tests")
    this.mappings["/public/tests"] = testDirectory
    this.mappings["/app/tests"] = testDirectory
    this.mappings["/tests"] = testDirectory

    include "/wheels/functions.cfm";
}

This is so Wheels's internal CFC references can find your application code. You might wonder why we need all those individual "sub"-mappings of /app, instead of just one mapping to /app. This is because somewhere in the bowels of /wheels/functions.cfm that mapping is created, and given hard-coded assumptions about the codebase layout, it ends up pointing to the wrong place. At the same time, that code also used the paths I map above. Fortunately /app is never used by itself; it's always with one of those suffixes above.

Step 8: remap some include paths in src/config/settings.cfm

Add this lot to the bottom of src/config/settings.cfm:

set(eventPath = "../src/events")
set(filePath = "../src/files")
set(modelPath = "../src/models")
set(modelComponentPath = "../src/models")
set(pluginPath = "../src/plugins")
set(pluginComponentPath = "../src/plugins")
set(viewPath = "../src/views")

set(controllerPath = "/app/controllers")

set(webPath = "")
set(imagePath = "images")
set(javascriptPath = "javascripts")
set(stylesheetPath = "stylesheets")

set(wheelsComponentPath = "cfmlInDocker.wheels")

Values that "worked" here were all set via trial and error, because the CFWheels code is very… opaque, and there's no documentation I can find on them. Certainly I needed these values for the controllers and views to be located (that's as much as I've looked at with CFWheels so far). I expect the component paths need to be dotted rather than a file-system path: I'll try to find out this evening.

Conclusion

Ideally at some juncture CFWheels will be reconfigured to not assume where the application implementation code is, and use a mapping (that it is provided at bootstrap) to find it instead. And similarly path all its own code on a /wheels mapping, then the framework could reside anywhere. It should just need to work like this:

// my Application.cfc
component extends=wheels.Application {

    this.appMapping = "/myApp"
}

And in the setup docs say "make sure to create these two server mappings". It's just easy. Which is what CFWheels is supposed to be all about, right?

Righto.

--
Adam