Feature #3199
closedConfiguration option to set the statuscode using url.access-deny
Description
I want to limit the allowed HTTP methods to a backend
$HTTP["request-method"] !~ "^(GET|HEAD)" { url.access-deny = ("") }
This results always in a return code 403 (hard coded in
mod_access.c
),but I want to configure code 405 (METHOD not allowed), something like
url.deny-status = 405
.(Haproxy:
http-request deny deny_status 405 if ...
).
I know, that this can be achieved with mod_magnet
and a lua script, but on a backend with thousands of requests the proposed solution is much more efficient.
Updated by gstrauss over 1 year ago
I know, that this can be achieved with mod_magnet and a lua script, but on a backend with thousands of requests the proposed solution is much more efficient.
mod_magnet with lua can be very, very fast, e.g. mod_magnet is faster serving a small static response than using mod_staticfile to serve a small file. In any case, both are very, very fast and the difference is measurable in microbenchmarks, but not noticeable in real-world use.
I think that you're right that a specialized directive will be slightly faster than mod_magnet in microbenchmarks, but I am not sure it will be noticeably faster in practice (... yes, base memory use will be a bit higher with lua). Still, let's test that. You can hard-code a response code in mod_access.c and compare performance to mod_magnet and a lua script. I'll test performance of a url.deny-status
this weekend.
The naming url.deny-status
might be confused as applying to more than just mod_access, so that is another consideration.
Updated by gstrauss over 1 year ago
https://www.rfc-editor.org/rfc/rfc9110#name-405-method-not-allowed
15.5.6. 405 Method Not Allowed
The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target > resource. The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods.
According to the RFC quoted above, 405 Method Not Allowed also requires response header Allow. While that might be achieved with additional config using mod_setenv, this is another reason against your proposed feature for a new url.deny-status
config option. lighttpd aims to be compliant with HTTP and related RFCs, and as currently described, this new config directive seems too narrowly focused. It is likely to be misused without also setting the "Allow" response header.
I do understand that there is a barrier to entry of learning some lua in order to use mod_magnet. That is why I have tried to provide many examples in ModMagnetExamples and in AbsoLUAtion.
Also, the overhead of using lua is amortized if you have more than one conditional in the lua script which is handled per-request. Using a lua script with mod_magnet might actually be faster than a complex set of regular expressions in lighttpd config syntax.
The following lua script (quickly written; untested) might be just as fast as using a regular expression in your $HTTP["request-method"] !~ "^(GET|HEAD)" { url.access-deny = ("") }
local r = lighty.r local method = r.req_attr["request.method"] if (method ~= "GET" and method ~= "HEAD" and method ~= "POST") then r.resp_header["Allow"] = "GET, HEAD, POST" r.resp_header["Content-Type"] = "text/plain" r.resp_body:set("405 Method Not Allowed") return 405 end -- (could add additional request conditions here) return 0
Aside: for those looking to implement application level firewalls, mod_magnet can be much faster than using libmodsecurity. However, you have to write your own policy.
For those looking to use libmodsecurity and libmodsecurity policies:
You can use libmodsecurity from mod_magnet if you think it can help protect your site, but using libmodsecurity has a large CPU and memory cost and reduces performance. AbsoLUAtion mod_security
Another libmodsecurity option: I have an experimental branch "mod_security3" in my github gstrauss/lighttpd1.4 repo, with the commit message
[mod_security3] new module using libmodsecurity (experimental) ./configure --with-libmodsecurity Note: libmodsecurity has a humongous list of dependencies libmodsecurity is not near the performance level of lighttpd, but that is also not the goal of libmodsecurity. Using mod_security3 will result in higher memory use, higher CPU use, and slower responses compared to using lighttpd without mod_security3.
Updated by gstrauss over 1 year ago
- Status changed from New to Need Feedback
I know, that this can be achieved with mod_magnet and a lua script, but on a backend with thousands of requests the proposed solution is much more efficient.
Before I proceed further with this feature request, please help quantify "much more efficient". Yes, virtual memory use will increase, but RES - SHR should be a much smaller increase. Yes, I expect CPU will increase slightly, but lighttpd mod_magnet with a simple lua script can easily respond to > 10k requests per second on a single CPU (microbenchmark).
An alternative which might be marginally faster than my post above -- and probably just as performant as url.access-deny = ("")
-- would be lighttpd config
$HTTP["request-method"] != "GET" { $HTTP["request-method"] != "HEAD" { $HTTP["request-method"] != "POST" { magnet.attract-raw-url-to = ("/path/to/reject-405.lua") } } }
which executes the lua only when the method is not GET, HEAD, or POST, along with lua script reject-405.lua:
local r = lighty.r r.resp_header["Allow"] = "GET, HEAD, POST" -- might also include OPTIONS r.resp_header["Content-Type"] = "text/plain" r.resp_body:set("405 Method Not Allowed") return 405
This is not as extendable as passing all requests through a single lua script for multiple site policy checks, but is a very fast though very specific solution tailored precisely to the original post.
Updated by flynn over 1 year ago
weighttp -n 1000000 -k -t 2 -c 50 ...
, client and server on the same host):
- small file: 73.2k req/sec
- small file with url.access-deny: 71.1k req/sec
- small file with lua-script: 63.4k req/sec
- small file with lua-script (luajit): 66.5k req/sec
(lighttpd does not build without patching with luajit, but this would be another issue ...)
To be fair: my SCGI-backend can only deliver up to 8k req/sec on the same hardware, so in real world use this NOT an issue.
So my assumption much more efficient
is not true, ticket can be closed.
Updated by gstrauss over 1 year ago
- Status changed from Need Feedback to Wontfix
Aside: I'd prefer to mark this issue with status "Demurred" or "Not Accepted", but am reusing "Wontfix" instead of creating a new status in the issue tracker.
FYI: luajit is based on Lua 5.1. Using Lua 5.4 is generally recommended unless you are doing something very specific and have tested and measured that using luajit is substantially faster. [Edit: apparently this is incorrect; see post below]
For my own benchmarks, I found the following to be essentially the same speed (81k req/s using HTTP over TCP on localhost to serve an empty file) for the (expected) default case of GET, HEAD, and POST requests. I found essentially the same performance for serving the (expected) error case when I changed "GET" to "NOT" below (to avoid matching "GET" for my benchmark "GET" requests).
server.modules += ("mod_access") $HTTP["request-method"] != "GET" { $HTTP["request-method"] != "HEAD" { $HTTP["request-method"] != "POST" { url.access-deny = ("") } } }
versus
server.modules += ("mod_magnet") $HTTP["request-method"] != "GET" { $HTTP["request-method"] != "HEAD" { $HTTP["request-method"] != "POST" { magnet.attract-physical-path-to = ( "/dev/shm/405.lua" ) } } }
with
/dev/shm/405.lua
:local r = lighty.r r.resp_header["Allow"] = "GET, HEAD, POST" -- might also include OPTIONS -- let lighttpd return default 405 error document return 405
For benchmarking stability, I used taskset 0x1 lighttpd -D -f ...
for lighttpd, and used taskset 0x2 nice weighttp ...
for weighttp
Using h2load
for HTTP/2 instead of weighttp
, and using the same config as above, I benchmarked over 280k req/s for both success and 405 failure cases.
I hope this helps to convince you that mod_magnet is a first-class tool in lighttpd, and can be very flexible and very useful even on the critical path.
BTW, at the level of the microbenchmark pushing 280k+ req/s, $HTTP["request-method"] !~ "^(?:GET|HEAD|POST)$"
benchmarked to be marginally slower than the above config using three equality config tests, though if you added additional methods such as OPTIONS, the regex might edge out the increasing number of config conditions. In all cases, all of theses configs are very fast and without noticeable difference in practical real-world use.
Updated by gstrauss over 1 year ago
Hah! The original lua script I posted, which checks the method string in lua rather than using lighttpd conditions, is slightly faster (290k+ req/s) in the microbenchmark.
Updated by gstrauss over 1 year ago
I (erroneously) wrote:
FYI: luajit is based on Lua 5.1. Using Lua 5.4 is generally recommended unless you are doing something very specific and have tested and measured that using luajit is substantially faster. [Edit: apparently this is incorrect; see post below]
https://github.com/LuaJIT/LuaJIT/issues/929 (comment by author of LuaJIT) suggests LuaJIT is more popular and more stable.
Updated by gstrauss over 1 year ago
@flynn did you need any modifications besides something like the following to build with LuaJIT? This appears to work for me (along with build system detection modifications)
--- a/src/mod_magnet.c +++ b/src/mod_magnet.c @@ -202,6 +202,12 @@ SETDEFAULTS_FUNC(mod_magnet_set_defaults) { #endif #if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM < 502 +#ifdef __has_include +#if __has_include(<luajit.h>) +#include <luajit.h> +#endif +#endif +#if !defined(LUAJIT_VERSION_NUM) || LUAJIT_VERSION_NUM < 20005 static lua_Integer lua_tointegerx (lua_State * const L, int idx, int *isnum) { @@ -211,6 +217,7 @@ lua_tointegerx (lua_State * const L, int idx, int *isnum) return *isnum ? lua_tointeger(L, idx) : 0; } #endif +#endif #if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM < 502 /* lua5.1 backward compat definition */
Updated by flynn over 1 year ago
src/mod_magnet.c
configure.ac
In your latest checkins you set lua_min_ver=0
in the LuaJIT case, this should be lua_min_ver=2.1
to get the current LuaJIT release.
Updated by gstrauss over 1 year ago
In your latest checkins you set lua_min_ver=0 in the LuaJIT case, this should be lua_min_ver=2.1 to get the current LuaJIT release.
LuaJIT is based on Lua 5.1 and I handle whether or not LuaJIT 2.0.5 or later is used (which provides lua_integerx()
), or else provide lua_integerx()
myself. That's why lua_min_ver=0
(disabling the version check) is fine when ./configure --with_lua=luajit
is specified.
Also available in: Atom