-
Notifications
You must be signed in to change notification settings - Fork 24
Introduction to File Endpoints
- Introduction to Endpoints
- The File Endpoint
- File Endpoint Configuration
- Restricting Access
- Customizing the Behavior of the Endpoint
- Getting endpoint specific URLs
- Differences Between cfcontent and Apache mod_xsendfile
In all simplicity, endpoints are simple and lightweight "servlet" like handlers that accept requests and respond to them as required. Since endpoints will be used for non-human interactions (non-GUI), they are purposely designed to be extremely lightweight, and singular in purpose by performing without the need of plugins, filters, subroutines or other Mach-II constructs that the full Mach-II event request lifecycle provides. However, an endpoint does have the ability to invoke the full Mach-II request lifecycle if required. Since endpoints are registered with the framework itself, the deployment of Mach-II applications that leverage other technologies would be simplified.
In the end, endpoints make it simple to expose services such as REST, file serving, etc. that leverage Mach-II's mature application structure. All endpoints have full access to Mach-II's ColdSpring integration (via the depends
attribute), which provides hooks into other features like Mach-II's Caching and Logging. We've worked hard to make it as easy as possible for developers to quickly create intuitive, high-performance API "servlets."
An endpoint should respond in a manner that is valid for the type of endpoint it is. In this case, the file endpoint use cfcontent to serve the user the requested file. Alternatively, the endpoint can be configured send a HTML header so the document could be served by Apache x-sendfile so the CFML engine does not have to tie up threads serving files.
For example, the Mach-II Dashboard leverages a file endpoint so deployment of the Dashboard does not have to be complicated by any copying of image / javascript / css asset files into a webroot.
- Configurable serving engine (
cfcontent
or Apache mod x-sendfile) - Setting expires headers based on file type
- Serve file as attachment based on file type
- Conditionally return 304 "not modified" header if file modification time stamp and header sent by browser match
- Set a timestamp to the URL for browser caching
- Basic security measures such as restricting by file type, directory location, etc.
- You would like to serve protected files
- You would like to serve files located outside of your webroot
- You would like to serve files with timestamps on the URI and expire headers
- You would like to serve files using Apache mod_xsendfile
- You would like to serve files using
cfcontent
- You would like to serve a .cfm file as another file type (
mycss.cfm
->mycss.css
) because you need to dynamically generate output
The file endpoint is setup by adding it to your Mach-II configuration file just like a listener or plugin. Beware that endpoint names must be unique across your application and therefore the name of an endpoint in a module cannot be the same name in another module.
<endpoints>
<endpoint name="dashboard.serveAsset" type="MachII.endpoints.file.BaseEndpoint">
<parameters>
<parameter name="secure" value="false"/>
<parameter name="basePath" value="/MachII/dashboard/assets"/>
<parameter name="servingEngineType" value="cfcontent|sendfile" />
<parameter name="expiresDefault" value="access plus 365,0,0,0" />
<parameter name="attachmentDefault" value="false" />
<parameter name="timestampDefault" value="true" />
<parameter name="fileTypeSettings">
<struct>
<key name=".js,.css,.jpg,.gif,.png">
<struct>
<key name="expires" value="access plus 365,0,0,0"/>
<key name="attachment" value="false" />
</struct>
</key>
<key name=".pdf">
<struct>
<key name="expires" value="access plus 0,0,0,0"/>
<key name="attachment" value="true" />
<key name="timestamp" value="false" />
</struct>
</key>
</struct>
</parameter>
<parameter name="cfmFiles">
<array>
<element value="/css/basic.cfm" />
<element value="/css/dialog.cfm" />
<element value="/css/combined.cfm" />
<!-- Alternatively for all .cfm files in this
directory which is based off the basePath parameter -->
<element value="/css/*.cfm" />
<!--
Alternatively for all .cfm files in this directory which is
based off the basePath parameter and below
-->
<element value="/css/**/*.cfm" />
</array>
</parameter>
</parameters>
</endpoint>
</endpoints>
Parameter Name | Required | Default | Description |
---|---|---|---|
secure | no | false | Sets whether to use the value from urlBase or urlBaseSecure which indicates HTTPS/SSL URL base. |
basePath | yes | n/a | The base path to the directory in which to base the relative file paths from. This value will be evaluated with expandPath() which allows for CFML mappings, etc. |
servingEngineType | no - cfcontent or sendfile
|
cfcontent |
Which file serving engine to use. For more info, see below. |
expiresDefault | no | access plus 365,0,0,0 |
The default amount to set the expires headers. Uses an Apache like syntax which supports "access" or "modified" with days,hours,minutes,seconds timespan format or the value of none if no expires header should be used. |
attachmentDefault | no | false | A boolean indicating if the file should be served as an attachment so it will be downloaded by the browser. |
timestampDefault | no | true | A boolean indicating if the file timestamp should be append to the end of the URL to aid web browsers in caching. |
fileTypeSettings | no | n/a | A struct and keys of settings for specific file types. See example above for configuration of expires, attachment and timestamp settings. |
cfmFiles | no | n/a | An array or list of specific relative paths to .cfm file you want to allow the endpoint to server. You can use the directive of * as a wildcard to indicate patterns by creating an array or list of the specific file patterns. You can read more about the patterns we support in Simple Pattern Matcher documentation. |
One of our "security" measures by design is to the use of the basePath
parameter. The basePath
parameter allows you to restrict base directory in which you can serve files from. It would be bad security practice to pass the full absolute file path on the disk in the URL as this inadvertently gives hackers knowledge about the structure of your file system. Using the base path allows you pass a relative path to the base path and therefore not exposing a complete file path to the client. It is also recommended that you separate out any assets to be served by a file endpoint into their own directory to mitigate any security risk.
One of the features of the File endpoint is a feature that allows you to reference .cfm
files but serve the content of the file as another MIME type. This feature is very powerful in certain circumstances. For example, we use this feature in the Mach-II Dashboard to serve our CSS files (which are .cfm
) with custom paths to images. If you were to look at the /assets/css/basic.cfm
file in the Dashboard, you would see entries like this that allow us to create the correct path to the background image for the Dashboard:
body {
font: 0.75em/1.5em Arial, Helvetica, sans-serif;
margin:0;
background: ##FFF url(#BuildEndpointUrl("/img/headerBk.jpg")#) top left repeat-x;
}
When the browser needs the CSS file, it actually is calling a File endpoint for the dashboard. Mach-II processes the "mock" CSS file that is really a .cfm
file and pipes the results back to the browser as the correct MIME type for CSS (text/css
). This is done by using the pipe
directive (:css
) in the URI of the CSS file:
<view:style endpoint="dashboard.serveAsset"
p:file="/css/basic.cfm:css"
media="screen,projection"
outputType="inline" />
Because serving allowing the ability to serve .cfm
files as other MIME types is a potential security hazard, you must explicitly name the relative path to all .cfm
files you want to process and pipe through a File endpoint. You can indicate which .cfm
you desire in the cfmFile
parameter when configuring the endpoint.
By default, the file endpoint does not offer any advanced security measures such as checking if an user is logged in or has permission to access the requested file. This means if a client such as a web browser makes a request for a resource that exists then your configured file endpoint would serve the file. For example, this is not an issue for the Mach-II Dashboard which uses a file endpoint to serve JS / CSS / Image assets. The assets are publicly available and therefore not a security risk.
In a majority of circumstances, most developers will want to check to see if an user is logged or possibly if they have the privilege to access the requested file. This can easily be done by extending the file endpoint and providing your own customizations while leveraging the infrastructure already provided.
In our example, we're going to check to make sure that user is logged in. This example won't actually show how we are authenticating the user as that will depend on how you authenticate your users in your application. We simple extend the MachII.endpoints.file.BaseEndpoint
CFC and use the onAuthenticate
method.
Example:
<cfcomponent displayname="SecureServeAsset"
extends="MachII.endpoints.file.BaseEndpoint"
output="false">
<cffunction name="onAuthenticate" access="public" returntype="void" output="false"
hint="Runs between preProcess and handleRequest (only if implemented).">
<cfargument name="event" type="MachII.framework.Event" required="true" />
<!---
If the user is not logged in, then throw an exception which will cause the
default onException endpoint process to occur and short-circuit the request
--->
<cfif NOT StructKeyExists(session, "loggedIn") AND session.loggedIn EQ false>
<!---
You don't have to send 401 HTTP header as the onException point will
trap this exception and handle it
--->
<cfthrow type="MachII.endpoints.file.notAuthorized" message="The user is not logged in." />
</cfif>
</cffunction>
</cfcomponent>
We are using the onAuthenticate
cut-point into the endpoint lifecycle. This point runs after preProcess
and before handleRequest
. If the user is not logged in, we simple set an HTTP header and set a CFML throw so the onException
cut-point in the endpoint lifecycle is invoke. By default, all Mach-II endpoints have "basic" onException
handling which can be customized by overriding the onException
method with our custom code.
It is extremely easy to use the file endpoint to generate and server custom files. In this example, let's pretend that we our website has multiple brands and each brand has a unique number assigned to it (i.e. 2697) that corresponds to a record of information in our fictitious database. We're going to accomplish this by extending the file endpoint with our own custom CFC.
<endpoints>
<endpoint name="serveBrandAsset" type="myApp.endpoint.ServeBrandAssetEndpoint">
<parameters>
<parameter name="basePath" value="/myApp/brand/assets"/>
<parameter name="servingEngineType" value="cfcontent" />
<parameter name="fileTypeSettings">
<struct>
<key name=".js,.css,.jpg,.gif,.png">
<struct>
<key name="expires" value="access plus 365,0,0,0"/>
<key name="attachment" value="false" />
</struct>
</key>
</struct>
</parameter>
<parameter name="cfmFiles">
<array>
<!--
Alternatively for all .cfm files in this directory which
is based off the basePath parameter
-->
<element value="/css/*.cfm" />
</array>
</parameter>
</parameters>
</endpoint>
</endpoints>
Remember where going to create a custom endpoint by extending the Mach-II file endpoint. Notice that in the cfcomponent
tag's extends
attribute that we are using MachII.endpoints.file.BaseEndpoint
.
The other important piece is that we are going to override the preProcess
method that we inherited from the base endpoint. Remember there are is logic in the inherited preProcess
method that still needs to run so we have to call super.preProcess(arguments.event)
so that stuff occurs or our endpoint will not function properly.
Just like any other Mach-II process, all endpoints have access to the event object. So you append query string parameters to file endpoint URLs. We'll be interfacing with a fictitious BrandManager
that is injected by ColdSpring using the depends
attribute. Once we have bran info, we put it into our event object. Because our asset (like .css file) is a .cfm file that is "rendered" by the file endpoint -- it has access to the event object.
<cfcomponent
extends="MachII.endpoints.file.BaseEndpoint"
depends="BrandManager">
<!---
PUBLIC FUNCTIONS
--->
<cffunction name="preProcess" access="public" returntype="void" output="false"
hint="Runs when an endpoint request begins. Override to provide custom functionality and call super.preProcess().">
<cfargument name="event" type="MachII.framework.Event" required="true" />
<!--- Run the inherited method first --->
<cfset super.preProcess(arguments.event) />
<!--- Only get the brand info if the request file is a .cfm file otherwise it's could be a jpg, gif, pdf, etc. --->
<cfif arguments.event.getArg("fileExtension") EQ "cfm">
<!---
Code to get brand info. This blackbox in this example, but in this case we'll use a
fictitious BrandManager that is injected. This is a struct.
--->
<cfset event.setArg("brandInfoStruct", getBrandManager().getBrand( arguments.event.getArg("brandId", 0)) />
</cfif>
</cffunction>
</cfcomponent>
Since we want to have a dynamic .css file, we're going to create a .cfm file so we can use variables.
Stylesheet.cfm
// Grab the logo path from our brand info struct
div#logo { background: url('img/#event.getArg("brandInfoStruct").logoPath#'); }
// We could even reference BuildEndpointUrl to a png file
div#background {
background: url('#BuildEndpointUrl(event.getArg("brandInfoStruct").backgroundPath)#');
}
It is as simple as using the view tag library and the style
custom tag. Notice that we are doing something special with the file path /css/Stylesheet.cfm:css
. We are saying to serve us a .cfm
page but using the :css
tells the file endpoint to pipe the results of our Stylesheet.cfm
file as a .css
file (with the right MIME type etc) for browsers.
<cfimport prefix="view" taglib="/MachII/customtags/view" />
<view:style endpoint="serveBrandAsset"
p:file="/css/Stylesheet.cfm:css"
p:brandId="2697"
media="screen,projection" outputType="inline" />
Getting URLs that are built specifically to a file endpoint is as simple as calling the buildEndpointUrl()
method in any framework aware component (view, listener, etc.).
Example:
<img src="#BuildEndpointUrl('dashboard.serveAsset', 'file=/img/logo.gif')#" />
BuildEndpointUrl()
method URL parameters (the second argument) accepts the following attributes either is a |
(pipe) list or struct:
URL Parameter Name | Required | Description |
---|---|---|
file | yes | The file path which is relative to the base path. |
pipe | no | The file type you want to pipe the requested file to. This allows you pipe a .cfm file with the MIME-type header of a .css file. |
attachment | no | The file name you would like the browser to download as an attachment. Leaving the value as "" indicates that you want the endpoint to compute the file name based off the relative file path. |
The attachment
URL parameter will override any settings defined in the endpoint configuration. The pipe
URL parameter is only useful if processing a .cfm
page to another MIME-type.
Also, all the view
tag library custom tags support endpoints such as img
, script
, style
and link
:
<view:script endpoint="dashboard.serveAsset" p:file="/js/prototype.js" outputType="inline" />
The file endpoint ships with the ability to use cfcontent which is built into your CFML engine or Apache mod_xsendfile. One of the major differences is that the use of cfcontent to serve files ties us the total number of threads your CFML engine will process at any given point. This is usually not an issue for small files, but can become a big problem as the size of the file increases and the total time it takes a client to download the file. Large files can take minutes to hours for clients that have limited bandwidth internet connections. This means you can take down your server by tying up all available threads with long downloading files with cfcontent.
To the rescue: mod_xsendfile for Apache webserver. This allows us to set an HTTP header in which the CFML request ends quickly and Apache looks for this header to serve the file natively. This allows request which would tie up a CFML engine thread to end quickly (10-20ms) and have the web server tier take care of serving the file (which it is designed to do better than any CFML engine could).
If you are going to be serving many files, large files or any combination of both, Team Mach-II highly recommends you install Apache mod_xsendfile on your server and use the the option in the Mach-II File Endpoint.
At this time, an equivalent ISAPI module for Microsoft IIS is not available. Please update this section if a replacement for IIS becomes available and file a ticket with Mach-II so this option can be supported for the File Endpoint.