Is it at all possible to redirect .cfm files using .htaccess?

So, I’ve recently made the switch on my local dev machine (macosx) to lucee from acf. It mirrors production perfectly (ubuntu), with apache in the front. I then switched over 3 sites that I still had left on a production server running acf - some small changes and all seemed good. Until I started getting a bunch of 404’s.

It seems, I’m guessing due to the proxymatch setup in apache, that all .cfm/.cfc files bypass the .htaccess altogether. While this isn’t a problem for your normal rewrite feature (assuming you aren’t rewriting to a file ending in .cfm), it is a problem for any redirect/rewrite you want to do that ends in .cfm/.cfc, including for example common security disallows blocking access to things like web.cfm/server.cfm. In this case I noticed it primarily due to a bunch of old redirects from when these sites had files ending in .cfm. I can move these to the 404 template, and perhaps that is better, but I would really like to learn if this is normal, or if it can be worked around? It obviously worked in acf.

So, in my virtualhosts file an entry has for example:

<Directory "/Users/Sites/XXX/">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
  </Directory>
  <Proxy *>
    Allow from 127.0.0.1
  </Proxy>
  ProxyPreserveHost On
  ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:8009/$1$2

And then in .htaccess, the following will work:

RewriteRule ^testing1234.html /1234.html [R=301,L]

But this doesn’t:

RewriteRule ^testingsdfa.cfm /bladadf.html [R=301,L]

I’m on the latest version of Lucee both development and production. I noticed this old thread Apache and .cfm mod_rewrite not working which seems to have the same problem, but I can’t figure out what exactly that user did to resolve things (set up mod_proxy from the .htaccess?). Any thoughts/ideas on the best way to proceed here. I’m also happy to throw out the .htaccess altogether if needed - I just want a long term fix that works the same in dev/production.

Thanks in advance for any pointers!

Here are some thoughts I had about this: I don’t think that

RewriteRule ^testingsdfa.cfm /bladadf.html [R=301,L]

works, maybe because mod_proxy might preceed in this situation? Nothing is overriding or excepting your proxypass from doing what it should, thus it would preceed and the condition check of your second rewrite rule wouldn’t even run.

Also, in my opinion your approach makes sense for testing and experimenting Apaches behaviour, but for an apps setup that doesn’t make much senses to me, because why would I want to catch and redirect a .cfm hit to redirect to a .html, when what I usually do with URLRewrite is the exact opposite: to simulate static files I’d catch .html and urlrewrite it to some .cfm, hidding all .cfm files from the client. Or I would map some path to a .cfm, like /articles/1234 to /articles.cfm?articleID=1234. I’d never have in URLRewrites left side condition a .cfm|.cfc, unless it would be for urlRewrite to a proxy with [p] flag. If there is some redirect from a .cfm or .cfc to another .html file, I’d always do that from within my cf application with cflocation or cfheader and not in apaches urlRewrite, because that would from my understanding the
more logical thing to do. But of course, you might have your reasons to do so and it also might be a totally valid decision to do so.

In your case you have two conditions that are true: one in proxy_pass and one in the urlrewrite. I’d say first conditon wins, unless you set up your proxy_pass to except

^testingsdfa.cfm

I don’t know if you can override
httpd.conf’s proxypass with an .htaccess, or to force so your URLrewrite as the winner over proxypass.

Yes, I think that’s exactly what is happening - the proxymatch is bypassing apache altogether for files ending in .cfc/.cfm and thus passing it onto lucee/tomcat which is doing its thing. You are of course right in your second paragraph which is what I meant with it normally not being a problem for re-writing, but there is one situation that is often recommended and which I think warrants consideration. This article by Pete Freitag (https://www.petefreitag.com/item/715.cfm) on securing Lucee (recommended by Lucee as well) includes the following:

You could also use mod_rewrite to block railo-context uri on all sites but one:
RewriteEngine ON
RewriteCond %{HTTP_HOST} !^admin\.example\.com$ [NC]
RewriteRule ^/railo-context.* [F,L]

Let’s say someone tries to load a file in there that ends in .cfc or .cfm - basically that would invalidate this security measure (as far as I can tell). There’s also a bunch of other posts about securing using .htaccess (I actually have a line in my .htaccess too blocking a bunch of things, but this is based on ACF).

In my case I inherited an old site which originally many years ago had lots of pages ending in .cfm and they ended up rewriting many of those. But to not lose SEO value they want the old pages 301 redirected, which I have traditionally handled in .htaccess. Now this particular use case I can move around easily enough, but it is rather annoying, and also inconsistent with ACF.

I’m also just very curious if anyone knows if this is even possible with lucee, and if so, where I need to be looking?

Yeah!!! Good thought. But I think that pete’s security configuration is before proxy pass happens, and that would happen in your global httpd.conf before and not in .htaccess. Of course you can do that things as you do in ACF, because it’s Apache related, not cfengine related. You just need to set it up in the correct logic.

Hmm, yeah, so put that block before the proxypass. Is it possible to trigger the loading/processing of the .htaccess in some way before the proxypass? That would be the ideal. It seems to me like these guys ended up with that solution, or that they maybe put their proxypass inside the .htaccess (not sure if that’s possible…): Apache and .cfm mod_rewrite not working

Hmmm… don’t think you can trigger .htaccess before your global proxypass.

However, what you want to get done with Lucee just the way ACF did, should work the same way with Lucee, because this is Apache related, and has nothing to do with the cfengine.

If you have access to apaches configuration files, you could try to deactivate that proxypass in the global file. Because you can’t use proxypass in your .htaccess you could try to get the job done as a URLRewirite to your proxy with a [p] proxy flag. See the link below:

See also:

But I don’,t know how this would affect mod_cfml and added proxy headers.

However, I’d love to see the configuration of the former Apache global, virtualhost and .htaccess configuration files to see what/where the real difference is.

So in ACF I basically had everything exactly as I have it now, except that my virtual hosts were shorter as none of this proxypass/match was necessary? Here’s an old virtualhost:

<VirtualHost *:80>
   DocumentRoot "/Users/Sites/xxx/"
   ServerName aaa.local
   <Directory "/Users/Sites/xxx/">
        Options +FollowSymLinks -Indexes -Multiviews
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

And a new one:

<VirtualHost *:80>
  DocumentRoot "/Users/Sites/XXX/"
  ServerName aaa.local
  <Directory "/Users/Sites/XXX/">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
  </Directory>
  <Proxy *>
    Allow from 127.0.0.1
  </Proxy>
  ProxyPreserveHost On
  ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:8009/$1$2
  ProxyTimeout 1200
</VirtualHost>

.htaccess right now is identical to before, but any redirects for .cfm files are not working. I guess the difference is apache would process everything first, and then coldfusion would take over, whereas here coldfusion files are being handed off earlier. I just need to figure out some way to do the proxy afterwards. I wonder if moving the ‘proxy’ rules outside of the virtual hosts to something loaded afterwards would work? So all of this is identical anyway for each virtualhost:

 <Proxy *>
    Allow from 127.0.0.1
  </Proxy>
  ProxyPreserveHost On
  ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:8009/$1$2
  ProxyTimeout 1200

Maybe pull that out and load it afterwards in some way?

But Sam, where was the proxypass or handler of your first setup happening? I can’t see any .cfm|.cfc in there, that tells Apache to pass the file request to the servlet container Tomcat, or JRun or whatever for ACF. In fact, in your ACF setup there has to be a .cfm handler somewhere, and that cannot be positioned before your urlRewrite rules.

I don’t really know - this just worked. I think I read somewhere ACF has a hacked version of tomcat or something, so users don’t have to deal with this maybe? The end of my httpd.conf used to look like this:

# Virtual hosts
Include /private/etc/apache2/extra/httpd-vhosts.conf

<IfModule proxy_html_module>
Include /private/etc/apache2/extra/proxy-html.conf
</IfModule>

<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>

Include /private/etc/apache2/other/*.conf
Include /private/etc/apache2/mod_jk.conf

The only difference loaded after the virtual hosts is the mod_jk has changed. This was the original cfm file (I don’t see any proxy stuff here either, so perhaps this was all applied afterwards in one of the loaded files from here?):

# Load mod_jk module
LoadModule    jk_module  "/Applications/ColdFusion11/config/wsconfig/1/mod_jk.so"
# Where to find workers.properties
JkWorkersFile "/Applications/ColdFusion11/config/wsconfig/1/workers.properties"
JkMountFile "/Applications/ColdFusion11/config/wsconfig/1/uriworkermap.properties"
# Where to put jk logs
JkLogFile "/Applications/ColdFusion11/config/wsconfig/1/mod_jk.log"
# custom environment variables
JkEnvVar REDIRECT_URL
JkEnvVar REDIRECT_REMOTE_HOST
JkEnvVar REDIRECT_PATH
JkEnvVar REDIRECT_QUERY_STRING
JkEnvVar REDIRECT_HTTP_ACCEPT
JkEnvVar REDIRECT_HTTP_USER_AGENT
JkEnvVar REDIRECT_REMOTE_ADDR
JkEnvVar REDIRECT_SERVER_NAME
JkEnvVar REDIRECT_SERVER_PORT
JkEnvVar REDIRECT_SERVER_SOFTWARE
# Where to put jk shared memory
JkShmFile "/Applications/ColdFusion11/config/wsconfig/1/jk_shm"
# Set the jk log level [debug/error/info]
JkLogLevel info
# Select the timestamp log format
JkLogStampFormat "[%a %b %d %H:%M:%S %Y] "
AddHandler jakarta-servlet .cfm .cfml .cfc .cfr .cfswf
DirectoryIndex index.cfm
Alias /CFIDE "/Applications/ColdFusion11/cfusion/wwwroot/CFIDE"
<Directory "/Applications/ColdFusion11/cfusion/wwwroot/CFIDE">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
<Files ~ ".hbmxml$">
Order allow,deny
Deny from all
</Files>

Hi Sam, I took some time, digged into the docs, googled, stackoverflowed and did lot’s of experiments myself. It was interesting.

Your old configuration is an old one that uses mod_jk. I don’t know how mod_jk behaves and that was something I didn’t want to try to see how it works. However, in my experiments I’ve seen that mod_proxy` wins over mod_rewrite, unless you set ‘mod_proxy’ preceeding, before ‘mod_rewrite’. So you need to set it into your virtualhost configuration behind the mod_proxy section. Only that way a mod_rewrite rule for .cfm would win the race with proxymatch .cfm.

Because I know you want to use .htaccess there is a workaround to reverse proxy with mod_rewrite using the [p] flag within .htacces in combination with mod_proxy of virtual host configurations. Not going to talk much now, better see it in action. Please be aware that I’m reverse proxying to 8888. If you want to use AJP, please adapt it accordingly. Better try it after checking these files with plain http reverse proxy to 8888

Try the following virtualhost:

<VirtualHost *:80>
  DocumentRoot "/Users/Sites/XXX/"
  ServerName aaa.local
  
<Directory "/Users/Sites/XXX/">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
  </Directory>

# log for debugging:
LogLevel alert rewrite:trace8

 # For securing admin, always use global Location, because it overrides mod_proxy/mod_rewrite
 # <Location /lucee/admin>
 #  Order deny,allow
 #  Deny from all
 # </Location>
	
 # Also use Location to deny file requests, it is global and it overrides mod_proxy/mod_rewrite
 # For regex consider using <LocationMatch regex>-Directive
 # <Location /test.cfm>
 #  Order deny,allow
 #  Deny from all
 # </Location>


	ProxyPreserveHost On
	ProxyPassReverse / http://127.0.0.1:8888/
    ProxyTimeout 1200
  </VirtualHost>

When having done that, set the following .htaccess to your webroot:

RewriteEngine On
RewriteBase /
#URL rewrite to redirect .cfm files to some static file
RewriteRule "^test.cfm(.*)$"  "index.html?RewriteRuleHTACCESS" [R,L]

# Pass request to proxy with mod_rewrite rule
RewriteRule  "^(.+\.cf[cm])(/.*)?$" http://127.0.0.1:8888/$1$2  [P]
RewriteRule  "^(.+\.cfml)(/.*)?$" http://127.0.0.1:8888/$1$2 [P]

That works on my machine. Please try it, would be nice to see if it works for you.

UPDATE: That assumption below is wrong, please discard!

But still: If having set ProxyMatch condition in virtualhost/apache2.conf, that matches the same condition of a RewriteRule set in the .htaccess, ProxyMatch wins and the RewriteRule in .htaccess is discarded. My solution/workaround of setting a rewriteRule flaged [p] works.

WOW! You are my hero Andreas!!

Admittedly, this did not immediately work, but then I remembered I had made some changes to server.xml, and on checking realized I’d only sorted secret for ajp (well, as it’s local test only I just disabled it).

    <Connector protocol="AJP/1.3"
               address="127.0.0.1"
               port="8009"
               secretRequired="false"
               redirectPort="8443"
               maxConnections="250" 
               keepAliveTimeout="10000" />

So anyway, I swapped out to the ajp protocol instead, reloaded apache and kaboom - it just works! This is ace for sure. Thanks for the explanation and examples too - a little higher up I mentioned a few days ago that I kind of read that thread from a few years ago to mean that they maybe stuck their ‘proxypass in htaccess’ and that’s basically exactly what this is, but even though I tried that a few times, I clearly failed without such clear examples. It’s nice too, because everything runs through apache and then only the cfm gets passed onward (though I will take those commented out security recommendations to heart!). I have to admit I’m a little unclear on what exactly the difference is between doing it this way or with proxymatch, but I’ll take it :slight_smile:

Is there an advantage to doing this with plain http reverse vs ajp? Thanks again!!

The proxypassreverse: is there a reason you have 8888 in there? I spent some time reading up on this and is the idea that it should show the ‘front server’, ie. apache, and hide the backend server so f.ex:
ProxyPassReverse / http://localhost/

Just trying to wrap my head around this, and what the best would be in my case.

Hahahha… I feel very honoured and glad you’ve been able to figure it out with some of my input. It makes me happy when an effort is crowned with success :smiley:

As far as I understand, AJP has some advantages over http. See

In my opinion it depends which you want to use, example… for development you don’t need it at all. Another point is that AJP needs additional connectors and further setup, and it might extend attack surface a little, because it’s an additional port and service (as shown in the past with Ghostcat). In production I’d use AJP over plain http for bigger sites, in local dev I’d always like use plain http.

Yes! 8888 is very important. Port 8888 is the port on which Tomcat is listening. Tomcat runs the servlet container Lucee.jar that is needed to run your .cfm files. What happens in a very simplified manner, let’s say you are running an Lucee app on http://example.com/ fronted with Apache2 and connected to Tomcat (with Lucee) on that domain as backend:

  1. You open a page with your browser e.g. http://example.com/mypage.cfm, by default it would be at http port 80.

  2. On that port your Apache2 is listening. In Apache2 you’ve setup a set of rules that then will decide what to do with your request.

  3. The proxyPassMatch condition “http://example.com/mypage.cfm” on “^(.+.cf[cm])(/.*)?$” would be true, so Apache2 knows it has to use the reverseProxy. It adds important request headers to the request (e.g. such headers as needed for mod_cfml) and Apache2 then “reroutes” the process to Tomcat that is listening on 127.0.0.1 port 8888.

  4. Tomcat gets the request with added headers and knows what to do with the cfm page. The headers have e.g. additional information for mod_cfml. Mod_cfml checks if the context has been already defined in Tomcat, if not it creates it.

  5. Tomcat passes the request to the servlet container application Lucee.jar. Lucee.jar runs your cfm page and Tomcat responds the HTTP request back to Apache2.

  6. Apache2 passes the resonse back by sending it to the client/browser.

Let’s say you wouldn’t add the port 8888, what would happen? Apache2 would pass the request to itself (to port 80), causing an endless loop. Don’t know if Apache2 has an inbuilt mechanism to avoid it, didn’t test it, but that would be the logical thing to happen.

You can pass the requests to Tomcat as plain http using port 8888 or pass it as AJP using port 8009. You decide, but you need to pass it one way or the other.

Localhost should also work. I like to stick more to IPs in a static environment and I check it with the Tomcat configuration in server.xml. Using plain IPs just doesn’t need additonal resolving names to IP, but as always: everything depends very much on the configuration you want.

Now imagine what would happen if the browser requests an image file e.g.
http://example.com/someimage.jpg”? Apache2 wouldn’t pass the request to Tomcat, it would simply pic up the file from the webroot, take care of everything else and send the file directly to the browser. That’s all.

8888 is very important. Port 8888 is the port on which Tomcat is listening. Tomcat runs the servlet container Lucee.jar that is needed to run your .cfm files.

I understand this bit, which is in the rewrite rule in htaccess - but the proxypassreverse in the virtualhost is I think supposed to be a reflection back to the calling page what the headers are, so by including 8888 in that you’d be exposing it, which you kind of want to avoid (I think at least?). I tested it without and it seems to work fine, but there might be some edge case I haven’t considered yet. With AJP this apparently isn’t necessary (some more reading after your post), so I think I might go that route. Mind you, this reminds me I have to stick some secret in there to ensure I’m protected against ghostcat.

Thanks again for all your help/advice. If someone else on a mac comes across this, hit me up in case you’re struggling. I have it pretty much all figured out now, so happy to pay it forward!