Project

General

Profile

Actions

Feature #3199

closed

Configuration option to set the statuscode using url.access-deny

Added by flynn about 1 year ago. Updated about 1 year ago.

Status:
Wontfix
Priority:
Normal
Category:
mod_access
Target version:
-
ASK QUESTIONS IN Forums:
No

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.

Actions #1

Updated by gstrauss about 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.

Actions #2

Updated by gstrauss about 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.

Actions #3

Updated by gstrauss about 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.

Actions #4

Updated by flynn about 1 year ago

My Benchmarks (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.

Actions #5

Updated by gstrauss about 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.

Actions #6

Updated by gstrauss about 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.

Actions #7

Updated by gstrauss about 1 year ago

  • Target version deleted (1.4.70)
Actions #8

Updated by gstrauss about 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.

Actions #9

Updated by gstrauss about 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 */
Actions #10

Updated by flynn about 1 year ago

No, I modified th same two areas:
  • 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.

Actions #11

Updated by gstrauss about 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.

Actions

Also available in: Atom