in cmdbox, I added the following to include a .hta...
# box-products
r
in cmdbox, I added the following to include a .htacces file. I read that it can be included, but not having much success after restarting the server, but does not mean it is a problem with commandbox, tuckey, etc. as it may be code related possibly, too. This is a fresh pull of a website and trying to get it running for the first time. During the restart, cmdbox was able to verify that the .htaccess file exists. Since the broad problem is that the website just does not work with or without .htaccess, is there a way to test that these rewrite rules are working?
server set web.rewrites.config=htdocs/.htaccess
b
@ryan While Tuckey suppports htaccess files, it's always been a bit spotty
Did you enable trace debugging for the tuckey rewrites?
Also, enable the status page and use it to confirm what rules Tuckey says it has loaded.
If there's not a lot of rewrite rules in the file, it may just be less work to pop them in as server rules in the
server.json
file.
r
Hey Brad, ok, I didn't know there is a status page. I will try to find how to set that up
I may actually do that in
server.json
r
There is not a lot in this .htaccess
but there are some things in there that I have not seen before and not exactly sure if it should be carried over or how to carry it over to
server.json
I will have to do some research.
While attempting to configure the log in cmdbox, I ran across this. Maybe I have to enable the log, then hit the webpage to make a log and then continue on with the following?
Copy code
server log --follow --rewrites
No log file found for 'F:\code\web\sites\test\'!
f:/code/.CommandBox/server/691EA0275822000979443F2EF19D8A91-test/lucee-5.3.8.206/logs/rewrites.txt
Enable Rewrite logging with [server set web.rewrites.logEnable=true] and ensure you are started in debug mode.
I just noticed it said that I have to start in debug mode
b
Well, I recommended trace a above
r
took a wild guess with
restart --debug
b
But both work
I assume you followed my instructions and have trace enabled already 🙂
trace has a lot more information in it
r
ah, so the log is in the server.out.txt
b
There should be a separate rewrite log
r
there, but nothing is writing to it
b
Copy code
server log --rewrites
Hmm, not sure then
r
it looks like it started to and then it is not doing it any more
I receive the following with your command above.
Copy code
CommandBox:amalga-ccpb> server log --rewrites
[ERROR] Rewrite utils.ModRewriteConfLoader: Unknown line: AddType 'text/html; charset=UTF-8' html
[ERROR] Rewrite utils.ModRewriteConfLoader: Unknown line: AddType 'text/css; charset=UTF-8' css
[ERROR] Rewrite utils.ModRewriteConfLoader: Unknown line: AddType 'application/javascript; charset=UTF-8' js
[ERROR] Rewrite utils.ModRewriteConfLoader: Unknown line: Options -Indexes
[DEBUG] Rewrite: processing request for /_admin
[DEBUG] Rewrite Output: needs no substitution
[DEBUG] Rewrite: processing request for /_admin
[DEBUG] Rewrite Output: needs no substitution
b
That command is just showing you the contents of the rewrite log file
r
yeah, I see... Some of the lines I mentioned that I have not seen before in .htaccess are the same lines that are erroring in my previous msg
I understand the point of it, but I just have not seen them used in .htaccess before
I'm thinking that tuckey doesn't understand them, either.
b
That's possible, like I said, the htaccess support is hit and miss
I've never used it myself
r
because of these errors, I don't know if the rest of the file is aborted. I do see that it is parsing line by line below the errors, but just not certain if they are truly working.
b
I'd just post your htaccess file here and we can help you convert the rules
r
thanks, Brad.
Copy code
#
#  Everything is UTF-8
#
AddType 'text/html; charset=UTF-8' html
AddType 'text/css; charset=UTF-8' css
#
# - correct way to serve js according to <http://www.ietf.org/rfc/rfc4329.txt>
# - IE doesn't like application/javascript but on the other hand it doesn't
#   obey Content-Type headers so it's ok
AddType 'application/javascript; charset=UTF-8' js


# disable directory browsing
Options -Indexes

#
#  The order of the rewrite rules is generally important
RewriteEngine On
RewriteBase /

#  If we are in the admin area or we already have the language variable skip this block of rules
RewriteCond %{REQUEST_URI} ^/(_admin|_docs|_cfc|assets|images)
RewriteRule .* - [S=6]
#
#  The language must be the first thing in the URL. If it's present then set
# and env variable, remove it from the URL and continue with other rules
#
#  e.g. en/dir/file.cfm; en/dir1/dir2/file.cfm; en/dir/file.php
RewriteRule ^(en|jp)/(.+/)*(.+)\.(cfm|php)                                 $1/$2$3.$4 [QSA]
#  e.g. en/; en/dir/; en/dir1/dir2/
RewriteRule ^(en|jp)/(.+/)*$                                               $1/$2      [QSA]
#  Details for a news, etc: /dir/dir/#fuseaction#/#id#-#name#
RewriteRule ^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$  $1/$2index.cfm?fuseaction=$3&id=$4&title=$5 [NC,NS,L,QSA]
#  Download asset /dir/dir/download_asset/#name#
RewriteRule ^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)(download_asset)/([^.]+\.\w{3,})-?.*$  $1/$2index.cfm?fuseaction=$3&id=$4 [NC,NS,L,QSA]
#  Download asset /dir/dir/products/#type#
RewriteRule ^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)products/(\w+)$  $1/$2index.cfm?fuseaction=product_details&id=$3 [NC,NS,L,QSA]
#  Simple fuseaction (/directory/directory/#fuseaction#)
RewriteRule ^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)$           $1/$2index.cfm?fuseaction=$3 [QSA]

RewriteCond %{REQUEST_URI} ^/(testbed|assets/script/charting_library)
RewriteRule .* - [S=1]

# Ignore revision numbers in JS and CSS file names
RewriteRule ^((?:[a-z0-9\-_\\]+/)*)(\d+)\.(.+)\.(js|css)$ $1$3.$4 [NC,NS,L]

RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?webtest.*$ [NC]
RewriteRule ^_data/.+$ - [F]
b
Nice, I learned a new thing today
Copy code
RewriteRule .* - [S=1]
I've never seen the skip flag. The rule above skips the next 1 rule
👍🏼 1
I sort of hate it, but lol
😆 1
It's like
goto
but even more brittle
The rules seems straightforward, but a few bits are confusing
Copy code
#  The language must be the first thing in the URL. If it's present then set
# and env variable, remove it from the URL and continue with other rules
• I'm not sure what it means by setting an "env var" • Despite that comment, all the rules below it seem to still expect a language to be present in the URL 🤔
r
I saw this as well
b
Most of these seems like just a basic regex rule
You may be able to approximate the primary rules just to get the site working and not worry about some of the more oddball stuff it's doing
r
I was thinking somehow that it was grabbing either "en" or "jp" from the url path and then removing and adding it somehow to an environment variable.
b
Also this...
Copy code
# Ignore revision numbers in JS and CSS file names
That's an interesting approach, lol. They get their cache busting in the browser, but keep the file names the same on disk!
I was thinking somehow that it was grabbing either "en" or "jp" from the url path and then removing and adding it somehow to an environment variable.
I know what the comments say. What I'm saying is that's not what the rules is doing!
Copy code
RewriteRule ^(en|jp)/(.+/)*(.+)\.(cfm|php)                                 $1/$2$3.$4 [QSA]
The
en
or
jp
is presumably capture group 1, whcih is totally being used in the final URL as `$1`'
r
hahaha, agreed. I couldn't find the code that backs the comments anywhere, either.
b
Let's try to get one of the basic ones working-- most of the main rules are a variation of this
Copy code
#  Details for a news, etc: /dir/dir/#fuseaction#/#id#-#name#
RewriteRule ^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$  $1/$2index.cfm?fuseaction=$3&id=$4&title=$5 [NC,NS,L,QSA]
r
this goes into server.json?
b
so to break it down • NC means no case • NS doesn't use the rule on "sub requests" but I'll be honest, I'm not even sure what that means or it it applies here • L means the last rule to process. That's doable, but there's some caveats to what what means in CommandBox (since it also skips any remaining internal rules) • QSA Means query string append
r
ok, I found docs on it in server.json. Would it be easier if I create a rewrites.json file?
b
That's up to you. It's nice to have it in server.json but the annoying thing is it's inside of JSON so if you have quotes or backslashes you've got to escape them
r
ugh, didn't know that
i'll keep it in server.json LOL
b
well, the JSOn has to be valid, lol
r
true
b
most regex rules can use the regex as-is but the substitutions may be a little different
r
ahhh, i just noticed there is a configReloadSeconds
b
ignore that
That's part of tuckey and it doesn't even work
We're doing server rules
r
ok
b
which are unrelated to tuckey
They use Undertopw's predicate language
r
lol ok
So, here's a simple regex based rewrite
Copy code
regex('(.*).jpg$') -> redirect('${1}.png')
In the server rule, the rewrite condition is more/less on the left and the rewriterule is more/less on the right
r
ok
writing this out
b
So I'd start with something like this
Copy code
regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> { rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' ); last; }
SInce the predicate lang uses the same
${foo}
syntax as CommandBox's JSON files, you have to escape them as
\${foo}
as well
That's ONLY if you have the rule in the JSON file, of course,
If you have a
rules.txt
file, then you don't have to escape anything at all, which is nice
r
ok, a txt file sounds perfect then
b
OK, I updated the example above to remove the escapes
👍🏼 1
Now do a
Copy code
start --console --trace
after the site starts, hit enter a few times to tell where the previous logging stops, hit one page you expect to rewrite, and then read through the logs which will include ALL built in rules as well
In fact, setting
Copy code
server set profile=none
will probably help with testing since it will turn off all baked in CommandBox rules to reduce trace output
r
ah ok include a --console
b
The trace logging will show you • what regex was run • what URL it was run against • what capture groups were captured • what the result of the predicate was (the regex) • if the handler ran (the rewrite)
r
I did the following to create a rules file. Does this seem fine?
Copy code
server set web.rulesFile=htdocs/rules.txt
Thanks for this, Brad. Following your orders and going to test it now
just curious, would
restart --console --trace
work just the same?
received an error with
start --console --trace
Copy code
[TRACE] Runwar: regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> { rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' ); last; }
[ERROR] java.lang.IllegalArgumentException: UT000045: Error parsing predicated handler string no handler named last known handlers are [disallowed-methods, allowed-methods, buffer-request, jdbc-access-log, http-continue-accept, secure-cookie, access-log, mark-secure, response-rate-limit, canonical-path, response-code, disable-cache, ssl-headers, trace, block-external, blocking, url-decoding, block-cf-admin, error-file, access-control, redirect, set, ip-access-control, samesite-cookie, request-limit, resource, compress, restart, clear, byte-range, eager-form-parser, set-error, done, rewrite, forwarded, stuck-thread-detector, reverse-proxy, jvm-route, learning-push, dump-request, proxy-peer-address, resolve-local-name, header, store-response, path-separator, uncompress, resolve-peer-name]:
regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> { rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' ); last; }

                                                                                  ^
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.error(PredicatedHandlersParser.java:729)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.handleHandlerNode(PredicatedHandlersParser.java:157)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.handleNode(PredicatedHandlersParser.java:114)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.handleBlockNode(PredicatedHandlersParser.java:147)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.handlePredicatedAction(PredicatedHandlersParser.java:137)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.handlePredicateOperatorNode(PredicatedHandlersParser.java:125)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.handleNode(PredicatedHandlersParser.java:117)
[ERROR] at io.undertow.server.handlers.builder.PredicatedHandlersParser.parse(PredicatedHandlersParser.java:91)
[ERROR] at runwar.Server.startServer(Server.java:668)
[ERROR] at runwar.Start.main(Start.java:50)
[DEBUG] Runwar: Running shutdown hook
[DEBUG] Runwar: shutdown hook:stopServer()
[INFO ] Runwar: ******************************************************************************
[INFO ] Runwar: *** stopping server 'amalga-ccpb' (socket 63639)
[INFO ] Runwar: ******************************************************************************
[DEBUG] Runwar: All deployments undeployed and underlying Undertow servers stopped
[TRACE] Runwar: Unhooking system streams logger
[DEBUG] Runwar: Stopped server
[DEBUG] Runwar: shutdown hook joining main thread
[DEBUG] Runwar: Shutdown hook finished

Server's output stream closed. It's been stopped elsewhere.

Stopping server...
b
restart --console --trace
work just the same?
No, because the restart command only accepts a few params, and console and trace aren't among them 🙂
Copy code
restart ?
received an err
There's a syntax error in the rule. What have you done to try and find it? 🙂
I don't see the issue off the top of my head, but in a scenario like this, I simplify until it works and then add stuff back in slowly
I'd start by removing the
{}
and the
last;
bits from the rule
r
nothing yet HAHA, I will look in one second. Doing 3 things at once
b
Copy code
regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' )
r
ok, back, sorry
Thanks, Brad. I compared and see that the problem is/might be the curly braces
testing now
and last was removed
b
Not sure what you're saying
I don't know if the last version I pasted above will work, I'm just suggesting you try it as a test
To help narrow down the error
r
it's not erroring
just mentioning the comparison between rev 1 and 2 of the rule you made
b
The first rule will prob work if you take out the second semi colon
r
right now it's in a redirect loop and then erroring
b
I think I remember the undertow bug there
r
trying it
tried it by taking out the second
;
but it errored
Copy code
[TRACE] Runwar: regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> { rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' ); last }
[ERROR] java.lang.IllegalArgumentException: UT000045: Error parsing predicated handler string no handler named last known handlers are [disallowed-methods, allowed-methods, buffer-request, jdbc-access-log, http-continue-accept, secure-cookie, access-log, mark-secure, response-rate-limit, canonical-path, response-code, disable-cache, ssl-headers, trace, block-external, blocking, url-decoding, block-cf-admin, error-file, access-control, redirect, set, ip-access-control, samesite-cookie, request-limit, resource, compress, restart, clear, byte-range, eager-form-parser, set-error, done, rewrite, forwarded, stuck-thread-detector, reverse-proxy, jvm-route, learning-push, dump-request, proxy-peer-address, resolve-local-name, header, store-response, path-separator, uncompress, resolve-peer-name]:
regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> { rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' ); last }
b
I was thinking it was this error, but I can't recall https://issues.redhat.com/browse/UNDERTOW-1740
Oh, my bad, the handler is
done
not
last
lol
And you need to not have a semi after it
Here's an example from inside commandbox
Copy code
regex('/\.') and not path-prefix(.well-known) -> { set-error( 404 ); done }
r
ah ok, thanks, changing it now
receiving this error when it is starting up
Copy code
[ERROR] Mar 31, 2022 4:48:21 PM javax.mail.Session loadResource
WARNING: expected resource not found: /META-INF/javamail.default.address.map
server is still running, however
b
I don't know what that means, but it has nothing to do with whatever we're doing, lol
It's just a random javamail error
👍🏼 1
r
am I able to make this work ?
Copy code
RewriteCond %{REQUEST_URI} ^/(_admin|_docs|_cfc|assets|images)
RewriteRule .* - [S=6]
b
There is no direct "skip" sort of flag in undertow's rewrites
but there's probably a way to approximate it
That rules skips the next 5 rules when the condition is met
but it seems like you could just skip all remaining rules and have the same basic thing
So put the condition in a regex() predicate and just make the handler be
done
It's basically skipping the fusionaction rewrites for those folders
You can also do some cool nested stuff, which really is impossible to read if you're not in your own file such as this
Copy code
equals( foo, foo) -> {
    equals( foo, foo) -> rewrite( /foo );
    equals( foo, bar) -> rewrite( /bar );
    equals( foo, baz) -> rewrite( /baz );
} else {
    set(attribute='%{o,css}', value='false');
    path(/whee) -> response-code( 404 );
}
Which means you SHOULD be able to do something alone the lines of
Copy code
not regex( case-sensitive=false, pattern='^/(_admin|_docs|_cfc|assets|images)' ) -> {
    regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> {
        rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}' );
        done;
    }
    regex( case-sensitive=false, pattern='^another pattern') -> {
        rewrite( 'another rewrite' );
        done;
    }
}
@ryan ☝️
r
Very cool, thank you for the explanation and examples
I'm going to try this
b
Which is 100 times nicer than "skip the net 6 rules" etc
r
Lol no kidding
b
Both of those examples above DO parse in undertow. I tested them quickly
I actually didn't realize you could nest predicates until today, lol
I knew you could have two handlers for a single predicate inside
{}
, but I had never tried nesting more predicates
r
That's awesome and very helpful
Thank you, @bdw429s. Btw, what are you using to test?
b
Oh, I was just running a local server in an empty folder to toy around with it
Copy code
❯ cat server.json
{
    "profile":"none",
    "web":{
        "rulesFile":"rules.txt"
    }
}
And then I was playing around in the rules.txt file and restarting the server
👍🏼 1
r
Good morning, @bdw429s! Possible first issue: After working on this website setup, reading through documentation regarding rewrite rules, and converting the rewrite rules, I noticed that there are forward slashes in the regex that are not escaped. I thought, well maybe the forward slash is supposed to be the boundaries special character, except it only contains the beginning boundary and not including the end boundary. I then validated the regex and sure enough, it errored on the forward slash. How can the regex work appropriately with unescaped forward slashes and only one forward slash in the regex? Maybe it determines one side of the boundary, but I have never seen or read anything about the possibility of using a left boundary without the right. It should error, but it does not error except in one situation in the Lucee server startup, which I believe was a misguided error meant for something else and I misinterpreted it, which led me down this path about noticing the forward slashes, which leads into the following second issue. Possible second issue: I believe there is an issue possibly in nesting the predicates maybe when there is only one nested predicate, but I have not fully tested this part, yet. It got late... Having this error on one nest predicate brought to the question, what's the point of nesting one predicate? Maybe I can use an
and
in the predicate as two regex statements in one predicate. It stopped erroring and seems to work, but not sure yet, because the website is still not coming to life with these rules in place, which leads me to believe that the forward slashes may have something to do with this issue.
I tested .htaccess in an online tester and apparently an unscaped forward slash is allowed.
can't find an undertow online tester, unfortunately
I may have this working!
b
@ryan Oops, sorry. I saw your messages this morning but they got marked read on my phone and I forgot about them
This week is our Ortus retreat so we're all packed in a conference room hacking on stuff
So to address what you basically figured out above, there's nothing special about forward slashes (
/
) in regex so there's no need to escape anything there
The same regex rules that applied in htacces will apply to any regex engine, so you shouldn't need to worry about that
what's the point of nesting one predicate? Maybe I can use an
and
in the predicate as two regex statements in one predicate
Just brevity. Why do
Copy code
if( condition1 && conditionA ) {
} else if( condition1 && conditionB ) {
} else if( condition1 && conditionC ) {
} else if( condition1 && conditionD ) {
}
when you can do
Copy code
if( condition1 ) {
  if( conditionA ) {
  } else if( conditionB ) {
  } else if( conditionC ) {
  } else if( conditionD ) {
  }
}
That's not too hard to read, but consider • conditionA is likely much more characters in real life • If you change conditionA, you've got to edit 4 places
the website is still not coming to life with these rules in place, which leads me to believe that the forward slashes may have something to do with this issue.
Quit worrying about what you think the issue may be and figure out what the actual issue is 🙂 So for a given page that's not working • which specific rule should be kicking in • how is that rule supposed to work • what about the rule isn't working like it needs to? • Fix it Bullets 1 and 2 are up to you, bullet three should be evident from the trace console logs that show what the rules are doing.
r
Lol, well, i already went through those bullets :-) i got everything working
👍 1
Here is my rules.txt, so far.
Copy code
not regex( case-sensitive=false, pattern='^/(_admin|_docs|_cfc|assets|images)' ) -> {
    regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)/(\d+)-?(.*)$') -> {
        rewrite( '${1}/{$2}index.cfm?fuseaction=${3}&id=${4}&title=${5}%q' );
        done;
    }
    regex( case-sensitive=false, pattern='^(en|jp)/(.+/)*(.+)\.(cfm|php)') -> {
        rewrite( '$1/$2$3.$4%q' );
        done;
    }
    regex( case-sensitive=false, pattern='^(en|jp)/(.+/)*$') -> {
        rewrite( '$1/$2%q' );
        done;
    }
    regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)(download_asset)/([^.]+\.\w{3,})-?.*$') -> {
        rewrite( '$1/$2index.cfm?fuseaction=$3&id=$4%q' );
        done;
    }
    regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)products/(\w+)$') -> {
        rewrite( '$1/$2index.cfm?fuseaction=product_details&id=$3%q' );
        done;
    }
    regex( case-sensitive=false, pattern='^(en|jp)/((?:[a-zA-Z0-9\-_\\]+/)*)([a-zA-Z0-9\-_\\]+)$') -> {
        rewrite( '$1/$2index.cfm?fuseaction=$3%q' );
        done;
    }
}
not regex( case-sensitive=false, pattern='^/(testbed|assets/script/charting_library)' ) and regex( case-sensitive=false, pattern='^((?:[a-z0-9\-_\\]+/)*)(\d+)\.(.+)\.(js|css)$' ) -> {
	rewrite( '$1$3.$4%q' );
	done;
}
regex( case-sensitive=false, pattern='!^http(s)?://(www\.)?ccpb.*$') -> {
	rewrite('^_data/.+$%q');
	done;
}
The trace came in really handy. Thank you so very much for your help in this, @bdw429s!
👍 1
next on my agenda is learning cbsecurity this weekend! I do hope your weekend is going to be amazing at your retreat! Have fun!
b
@ryan That's a pretty sweet real-life example of server rules. Most of my docs are super simple contrived examples. Would you mind tossing a post on the community forum that just shows what you came up with so people can see a nice example of nested rules and stuff?
r
no problem
I'm tweaking it a bit at the moment and will send it up when it is ready 🙂
Wrote up an article. Again, thank you so much for all of your help, @bdw429s! https://community.ortussolutions.com/t/converting-htaccess-rules-to-undertow-text-based-rules-with-commandbox/9188
👍 1
b
@ryan Awesome, thanks!
I noticed you have
Copy code
rewrite( '$1/$2?fuseaction=$3' );
and not
Copy code
rewrite( '${1}/${2}?fuseaction=${3}' );
Can you confirm that syntax works? I was thinking about this the other day since I was thinking I had seen that before, but when I looked through the official undertow docs again I couldn't find any references to it so I wasn't sure.
If
$1
does for sure work instead of
${1}
I'd like to change the rest of my examples over to use that since it doesn't need to be escaped as
\${1}
in the
server.json
r
Doing some more testing on these Undertow rewrite rules. I will need to modify the tutorial. I am not getting some of the rules to work
1. Undertow does actually need the curly braces as you mentioned, such as
${n}
. 2. I cannot get the following to work for some reason. It is supposed to remove the numbers from the file, but it does not.
Copy code
Server Rules: Regex pattern [^((?:[a-z0-9\-_\\]+/)*)(\d+)\.(.+)\.(js|css)$] DOES NOT MATCH input [/assets/style/dist/529250.style.css] for HttpServerExchange{ GET /assets/style/dist/529250.style.css}.
Server Rules: Predicate [regex( pattern='^((?:[a-z0-9\-_\\]+/)*)(\d+)\.(.+)\.(js|css)$', value='%{RELATIVE_PATH}', full-match='false', case-sensitive='true' )] resolved to false for HttpServerExchange{ GET /assets/style/dist/529250.style.css}.
Figured out #2, I think. It works with a forward slash added after
^(
like
^(/(?:[a-z0-9\-_\\]+/)*)(\d+)\.(.+)\.(js|css)$
It finally rewrote and the css is showing up now that it is pointed to an existing file with the numbers in the filename. Now the original rules line with the 2 predicates works!
Copy code
Server Rules: Predicate [ not regex( pattern='^/(testbed|assets/script/charting_library)', value='%{RELATIVE_PATH}', full-match='false', case-sensitive='true' ) and regex( pattern='^(/(?:[a-z0-9\-_\\]+/)*)(\d+)\.(.+)\.(js|css)$', value='%{RELATIVE_PATH}', full-match='false', case-sensitive='true' )] resolved to true. Next handler is [PredicatesHandler with 2 predicates] for HttpServerExchange{ GET /assets/style/dist/529250.style.css}.
I just discovered
lucee-[version]\.predicateFile.txt
This helped me determine if I could place comments in the rules.txt file and after Undertow processes the rules.txt file, the output is saved into the .predicateFile.txt.
#
is the comment line character.
I am noticing this error related to Undertow.
Copy code
[DEBUG] io.undertow.request.error-response: Setting error code 500 for exchange HttpServerExchange{ GET /}
java.lang.RuntimeException: null
        at io.undertow.server.HttpServerExchange.setStatusCode(HttpServerExchange.java:1484) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.spec.HttpServletResponseImpl.setStatus(HttpServletResponseImpl.java:287) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at javax.servlet.http.HttpServletResponseWrapper.setStatus(HttpServletResponseWrapper.java:192) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at lucee.runtime.PageContextImpl.handlePageException(PageContextImpl.java:2022) [5.3.8.206.lco:?]
        at lucee.runtime.util.PageContextUtil.getHandlePageException(PageContextUtil.java:227) [5.3.8.206.lco:?]
        at lucee.runtime.exp.CatchBlockImpl.castToString(CatchBlockImpl.java:225) [5.3.8.206.lco:?]
        at lucee.runtime.op.Decision.isString(Decision.java:965) [5.3.8.206.lco:?]
        at lucee.runtime.op.Decision.isCastableToString(Decision.java:977) [5.3.8.206.lco:?]
        at lucee.runtime.op.Decision.isCastableTo(Decision.java:1339) [5.3.8.206.lco:?]
        at lucee.runtime.type.UDFImpl.castToAndClone(UDFImpl.java:112) [5.3.8.206.lco:?]
        at lucee.runtime.type.UDFImpl.defineArguments(UDFImpl.java:164) [5.3.8.206.lco:?]
        at lucee.runtime.type.UDFImpl._call(UDFImpl.java:342) [5.3.8.206.lco:?]
        at lucee.runtime.type.UDFImpl.callWithNamedValues(UDFImpl.java:207) [5.3.8.206.lco:?]
        at lucee.runtime.ComponentImpl._call(ComponentImpl.java:685) [5.3.8.206.lco:?]
        at lucee.runtime.ComponentImpl._call(ComponentImpl.java:572) [5.3.8.206.lco:?]
        at lucee.runtime.ComponentImpl.callWithNamedValues(ComponentImpl.java:1930) [5.3.8.206.lco:?]
        at lucee.runtime.util.VariableUtilImpl.callFunctionWithNamedValues(VariableUtilImpl.java:866) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl.getFunctionWithNamedValues(PageContextImpl.java:1766) [5.3.8.206.lco:?]
        at db_migrate_cfm$cf.call(/db_migrate.cfm:127) [?:?]
        at lucee.runtime.PageContextImpl._doInclude(PageContextImpl.java:1034) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl._doInclude(PageContextImpl.java:926) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl.doInclude(PageContextImpl.java:907) [5.3.8.206.lco:?]
        at application_cfc$cf.udfCall(/Application.cfc:24) [?:?]
        at lucee.runtime.type.UDFImpl.implementation(UDFImpl.java:106) [5.3.8.206.lco:?]
        at lucee.runtime.type.UDFImpl._call(UDFImpl.java:344) [5.3.8.206.lco:?]
        at lucee.runtime.type.UDFImpl.call(UDFImpl.java:217) [5.3.8.206.lco:?]
        at lucee.runtime.ComponentImpl._call(ComponentImpl.java:684) [5.3.8.206.lco:?]
        at lucee.runtime.ComponentImpl._call(ComponentImpl.java:572) [5.3.8.206.lco:?]
        at lucee.runtime.ComponentImpl.call(ComponentImpl.java:1911) [5.3.8.206.lco:?]
        at lucee.runtime.listener.ModernAppListener.call(ModernAppListener.java:437) [5.3.8.206.lco:?]
        at lucee.runtime.listener.ModernAppListener.onApplicationStart(ModernAppListener.java:304) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl.initApplicationContext(PageContextImpl.java:3135) [5.3.8.206.lco:?]
        at lucee.runtime.listener.ModernAppListener._onRequest(ModernAppListener.java:120) [5.3.8.206.lco:?]
        at lucee.runtime.listener.MixedAppListener.onRequest(MixedAppListener.java:44) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl.execute(PageContextImpl.java:2460) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl._execute(PageContextImpl.java:2450) [5.3.8.206.lco:?]
        at lucee.runtime.PageContextImpl.executeCFML(PageContextImpl.java:2421) [5.3.8.206.lco:?]
        at lucee.runtime.engine.Request.exe(Request.java:45) [5.3.8.206.lco:?]
        at lucee.runtime.engine.CFMLEngineImpl._service(CFMLEngineImpl.java:1179) [5.3.8.206.lco:?]
        at lucee.runtime.engine.CFMLEngineImpl.serviceCFML(CFMLEngineImpl.java:1125) [5.3.8.206.lco:?]
        at lucee.loader.engine.CFMLEngineWrapper.serviceCFML(CFMLEngineWrapper.java:97) [lucee.jar:?]
        at lucee.loader.servlet.CFMLServlet.service(CFMLServlet.java:51) [lucee.jar:?]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:590) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.cfmlprojects.regexpathinfofilter.RegexPathInfoFilter.doFilter(RegexPathInfoFilter.java:47) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.tuckey.web.filters.urlrewrite.RuleChain.handleRewrite(RuleChain.java:176) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.tuckey.web.filters.urlrewrite.RuleChain.doRules(RuleChain.java:145) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.tuckey.web.filters.urlrewrite.UrlRewriter.processRequest(UrlRewriter.java:92) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.tuckey.web.filters.urlrewrite.UrlRewriteFilter.doFilter(UrlRewriteFilter.java:405) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:280) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:79) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:134) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:131) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.api.LegacyThreadSetupActionWrapper$1.call(LegacyThreadSetupActionWrapper.java:44) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:260) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:79) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:100) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.server.Connectors.executeRootHandler(Connectors.java:387) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:852) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2019) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1558) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1423) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1280) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
        at java.lang.Thread.run(Unknown Source) [?:?]
b
It works with a forward slash added after
@ryan yeah, that's one annoying difference between web servers is whether you're request URL always comes in with the leading slash.
foo
vs
/foo
I'm guessing Apache was a little different there.
r
that's correct, @bdw429s
b
Copy code
java.lang.RuntimeException: null
        at io.undertow.server.HttpServerExchange.setStatusCode(HttpServerExchange.java:1484) [runwar-4.7.2-SNAPSHOT.jar:4.7.2-SNAPSHOT]
Hmm, that one is really odd. It doesn't seem to be related at all to your rewrites. You have CF code in
/db_migrate.cfm
which has thrown an exception, and as part of that, Lucee is setting the reponse's status code to 500. But some sort of error has happened inside of Undertow while setting that. I can't say I've ever seen this before.
I see the code is running as part of onApplicationStart, but I can't image why that would matter
Oh, wait a minute
I don't think that's an error at all
it's just your trace/debug logging I'm guessing
here is the Undertow code
Copy code
if(statusCode >= 500) {
            if(UndertowLogger.ERROR_RESPONSE.isDebugEnabled()) {
                UndertowLogger.ERROR_RESPONSE.debugf(new RuntimeException(), "Setting error code %s for exchange %s", statusCode, this);
            }
        }
If the status code is 500, it logs to the console to let you know
I'm guessing this only appears in the console and not in the browser?
Also, does it go away if you turn off
trace
level debugging in the
server start
?
@ryan
r
correct
b
Ok, so it's nothign to worry about then
r
ok, thank you for noticing that
b
In the latest version of CommadBox, I opened up the runwar logging to capture more of what Undertow logs nativley
This is something that used to be suppressed, but is now flowing through
r
not a problem
It didn't seem like it was affecting anything when testing, but thought I would bring it up just in case
b
It's sort of necessary in our case, but Undertow doesn't know that.
👍🏼 1
FWIW, we can turn off that logger if we want
I have full control in Runwar of what undertow loggers we pass through
it used to just be a subset of them all, but I changed it to be all of them by default just to see what messages we weren't seeing 🙂
r
I'm working on more more rules issue that I'm figuring out right now. After I fix this, would it be possible that I can edit the post I made on the forum? I did not have edit rights after about a day. Maybe that is automated after roughly 24 hours or something?
b
Hmm, I should be able to change that
It's probably related to your trust level in the forum
Discourse keeps you on a short leash if it doesn't trust you
r
awesome, yeah that's fine. Better to see more than not seeing enough.
HAHAHA I guess I'm some sort of bad guy!
b
Try now
r
it allowed me one change and that was all I got! LOL
👍 1
b
No, you just have to use the forum for a while before it starts to raise your trust level, which unlocks different features
All to prevent spam really
r
there we go! Thank you, Brad!
👍 1
b
I just changed your trust level to "regular" user
r
I appreciate it, thank you.
💪 1
ok, going to fix this (hopefully last) rules issue and then edit the post.
👍 1
@bdw429s would you happen to know what the equivalent of the Apache's
${HTTP_REFERER}
would be for UnderTow?
I've been trying to figure this out, but not having much luck
I was looking at exchange attributes for UnderTow, but I believe those are for the handler and not the predicates
The following is the predicate that I believe is not quite correct, yet. I think the regex refers to the Apache
%{REQUEST_URI}
equivalent. However, I do not see a way to change it.
Copy code
regex( case-sensitive=false, pattern='!^http(s)?://(www\.)?ccpb.*$') -> {
	rewrite('^/_data/.+$');
	done;
}
b
I was looking at exchange attributes for UnderTow, but I believe those are for the handler and not the predicates
@ryan untrue. exchange attributes can be used anywhere. They are just a generic abstraction around any parts of the HTTP request or response you want to get or set.
I don't know if Undertow has a shortcut for referrer, but it's just a header so you can just get it by name
r
I think I found a way to do it yesterday
b
%{i,request_header_name} - Any request header
So you should just be able to do
Copy code
%{i,HTTP_REFERER}
The
i
stands for "incoming" so it's a request header
As opposed to
%{o,response_header_name} - Any response header
where the
o
stands for "outgoing" which are response headers
I think the regex refers to the Apache
%{REQUEST_URI}
equivalent.
Correct, unless you provide a specific exchange attribute for your regex to be applied to, it will look at the request URI
r
I did this
regex( case-sensitive=false, pattern='!^http(s)?://(www\.)?ccpb.*$', value="%h", full-match=true)
, but I think I may have to remove
case-sensitive=false
I have to test it more today.
b
But you can run the regex against any exchange attribute you like
Such as this example from the ex page
Copy code
regex(pattern="Googlebot", value="%{i,USER-AGENT}", case-sensitive=false ) -> set-error( 404 )
which looks at the user agent header
%h
is the "Remote host name". is that what you want?
That's not the same as the HTTP referrer
@ryan
Search this page for
%h
https://undertow.io/undertow-docs/undertow-docs-2.0.0/#built-in-handlers-2 It has all the exchange attributes listed that Undertow supports
If you're intending to match based on the HTTP referrer, you'll want your regex predicate to have this
Copy code
value="%{i,HTTP_REFERER}"
r
I need the HTTP referrer, but threw that in just to see what comes back as I think the regex with a value attribute will allow for the exchange attribute
b
I think the regex with a value will allow for the exchange attribute
I don't understand what you mean by that
The regex predicate will always use an exchange attribute to match against-- the question is which one will it use
by default, it's the request URI unless you tell it something different to look at
r
i meant the inclusion of the value attribute will allow me to use an exchange attribute
b
Will allow you to use a different exchange attribute than the exchange attribute being used by default
The regex predicate can't not use an exchange attribute 🙂
Also, a fun note about undertow-- it's predicate language is fully customizable. For example, if you wanted to be able to do something like
%HTTP_REFERRER
as a shortcut for
%{i,HTTP_REFERER}
, you can write a small Java class, drop it in the classpath, and it will get sucked in and added to the language
You can write custom predicates and handlers too
CommandBox actually does this
Predicates such as
cf-admin()
and handlers such as
block-external()
are my own creations and only exist if using undertow inside of CommandBox
👍🏼 1
r
very cool
Yeah, by testing, I realized it was using request URI by default.
b
You should write your own handler in java that implements all your rules simply called
Copy code
do-rewrites()
and the that will be all you have in your predicate file 😆
r
and then have the handler read .htaccess files correctly 😉
b
It would be interesting to write a CommandBox module that parsed an htaccess file and transated it to server rules on the fly. Sounds sort of easy, but probably harder that it sounds
Stuff like those funky skip directives in mod_rewrite would be rather difficult to automate IMO
Technically IIS rewrites can be as portable with a
web.config
file, but in my experience IIS useres rarely ever use that
r
Yeah, I was reading more about the mod_rewrite skips and while I understood it, they made it easier to understand in their description by saying to look at the skip as a conditional statement, which is basically all it is.
b
Just with terrible parsing semantics, lol
😆 1
Without proper "body" declaration (like undertows
{}
) , all it takes is for a developer to add or remove a line and break the entire flow
👍🏼 1
Imagine adding a new rule to the middle of the others and then trying to figure out why the last one is always firing...
r
HAHA Reminds me of the colspan attributes in td tags. You add a column and then everything gets out of whack, especially the ones in conditional statements that was overlooked inside an include of a 2000 line html page. I still have nightmares having dealt with those. LOL
🤣 1