ModMagnetExamples » History » Revision 13
Revision 12 (gstrauss, 2021-12-12 05:11) → Revision 13/32 (gstrauss, 2021-12-12 15:57)
{{>toc}}
h3. lua examples of lighttpd modules
mod_magnet with lua scripts can allow for customized alternatives to many lighttpd modules.
The following are simple examples to demonstrate, but are not full reimplementations.
(examples below use mod_magnet API since lighttpd 1.4.60)
h4. lua mod_access
<pre>
-- reject access to paths ending in ~ or .inc
local path = lighty.r.req_attr["uri.path"]
if (string.match(path, "~$") or string.match(path, "%.inc$")) then
return 403
end
</pre>
h4. lua mod_alias
<pre>
local req_attr = lighty.r.req_attr
local path = req_attr["physical.path"]
if (not path) then return 0 end
local plen = path:len()
local basedir = req_attr["physical.basedir"]
local blen = basedir:len()
if (string.match(basedir, "/$")) then blen = blen - 1 end
if (0 == plen or plen < blen) then return 0 end
local url_fspath = string.sub(path, blen+1, -1)
-- remap /basedir/cgi-bin/ to alternative filesystem path
if (string.match(url_fspath, "^/cgi-bin/")) then
-- prefix match and replacement should both end in slash ('/') or both not
local match_len = 9 -- length of "/cgi-bin/"
req_attr["physical.basedir"] = "/var/www/servers/www.example.org/cgi-bin/"
req_attr["physical.path"] = "/var/www/servers/www.example.org/cgi-bin/"
.. string.sub(url_fspath, match_len+1, -1)
end
</pre>
h4. lua mod_auth
<pre>
-- There are many many many different ways to implement mod_auth.
-- This is a simplistic example of HTTP Basic auth.
-- Arbitrarily complex auth could be implemented in lua.
local realm = "secure"
local r = lighty.r
function check_user_pass(user, pass)
-- (trivially) hard-code valid username and password (and realm)
-- (This should be replaced by something more appropriate for your system)
-- (could be table or could be read from an external file or database or ...)
-- (lighttpd mod_auth is much more advanced)
if (realm == "secure" and user == "admin" and pass == "supersecretpass") then
return true
else
return false
end
end
function unauthorized()
r.resp_header["WWW-Authenticate"] =
'Basic realm="' .. realm .. '", charset="UTF-8"'
return 401 -- 401 Unauthorized
end
local authorization = r.req_header["Authorization"]
if (not authorization) then return unauthorized() end
local b64auth = string.match(authorization, "^Basic%s+([%w+/=]+)")
if (not b64auth) then return unauthorized() end
-- base64 decode into username:password string
local auth = lighty.c.b64dec(b64auth)
if (not auth) then return unauthorized() end
local user, pass = string.match(auth, "^([^:]+):(.*)$")
if (not user) then return unauthorized() end
-- check authentication
if (not check_user_pass(user, pass)) return unauthorized() end
-- check authorization
-- (could see if authenticated user is authorized to access requested URI)
-- authenticated and (optionally) authorized
r.req_env["REMOTE_USER"] = user
r.req_env["AUTH_TYPE"] = "Basic"
</pre>
h4. lua mod_dirlisting
<pre>
local r = lighty.r
local path = r.req_attr["physical.path"]
-- check that path ends in '/'
if (not path or not string.match(path, "/$")) then
return 0
end
-- check that path exists and is a directory
local st = lighty.c.stat(path)
if (not st or not st.is_dir) then
return 0
end
-- list filenames in directory
for name in lighty.c.readdir(path) do r.resp_body:add({name, "\n"}) end
r.resp_header["Content-Type"] = "text/plain"
return 200
</pre>
h4. lua mod_evhost
<pre>
local server_root = "/var/www/servers/"
local req_attr = lighty.r.req_attr
local host = req_attr["uri.authority"]
if (not host) then return 0 end
local shard = string.match(host, "(%w)[^.]*%.[^.%]]+$")
if (not shard) then return 0 end
local docroot = server_root .. shard .. "/" .. host
local st = lighty.c.stat(docroot)
if (not st or not st.is_dir) then return 0 end
req_attr["physical.doc_root"] = docroot
req_attr["physical.basedir"] = docroot
req_attr["physical.path"] = docroot .. req_attr["physical.rel-path"]
</pre>
h4. lua mod_expire
<pre>
local path = lighty.r.req_attr["physical.path"]
if (not path) then
return 0
end
local suffix = string.match(path, "(%.[^./]*)$")
if (suffix == ".html") then
lighty.r.resp_header["Cache-Control"] = "max-age=" .. (lighty.c.time()+300)
return 0
end
if (suffix == ".css" or suffix == ".js") then
lighty.r.resp_header["Cache-Control"] = "max-age=" .. (lighty.c.time()+3600)
return 0
end
if (suffix == ".jpg") then
lighty.r.resp_header["Cache-Control"] = "max-age=" .. (lighty.c.time()+86400)
return 0
end
</pre>
h4. lua mod_extforward
<pre>
local r = lighty.r
local req_header = r.req_header
local xffor = req_header["X-Forwarded-For"]
if (xffor) then
local req_attr = r.req_attr
req_attr["request.remote-addr"] = xffor
local xfport = req_header["X-Forwarded-Port"]
if (xfport) then req_attr["request.remote-port"] = xfport end
local xfproto = req_header["X-Forwarded-Proto"]
if (xfproto) then req_attr["uri.scheme"] = xfproto end
end
</pre>
h4. lua mod_indexfile
<pre>
local req_attr = lighty.r.req_attr
local path = req_attr["physical.path"]
-- check that path ends in '/'
if (not path or not string.match(path, "/$")) then
return 0
end
-- check for index.php then index.html
local indexfiles = { "index.php", "index.html" }
for _, file in ipairs(indexfiles) do
if (lighty.c.stat(path .. file) then
req_attr["physical.path"] = path .. file
req_attr["uri.path"] = req_attr["uri.path"] .. file
return 0 -- let mod_staticfile or other module handle the file
end
done
</pre>
h4. lua mod_redirect
<pre>
-- redirect http to https
if (lighty.r.req_attr["uri.scheme"] == "http") then
local r = lighty.r
r.resp_header["Location"] = "https://"
.. r.req_attr["uri.authority"]
.. r.req_attr["request.uri"]
return 302
end
</pre>
h4. lua mod_rewrite
<pre>
-- redirect if file does not exist (not file, not directory, not anything else)
-- (This script must be called from magnet.attract-physical-path-to hook)
if (not lighty.c.stat(lighty.r.req_attr["physical.path"])) then
local req_attr = lighty.r.req_attr
local query = req_attr["uri.query"]
req_attr["request.uri"] = "/index.php?path="
.. req_attr["uri.path-raw"]
.. (query and ("&" .. query) or "")
return lighty.REQUEST_RESTART
end
</pre>
h4. lua mod_secdownload
<pre>
-- extract tokens from url-path
local req_attr = lighty.r.req_attr
local mac, protected_path, tsstr, rel_path =
string.match(req_attr["uri.path"], "^/download/([%w_-]+)(/(%x+)(/.*))$")
if (not mac) then return 0 end
local ts = tonumber(tsstr, 16)
if (not ts) then return 0 end
local li = lighty.c
-- validate timestamp
local timeout = 600 -- (10 mins) validity window
local t = li.time()
if (((t > ts) and (t - ts) or (ts - t)) > timeout) then
return 410 -- 410 Gone; URI will never be valid again
end
-- validate MAC
local secret = "12345-bad-secret" -- you MUST customize this
local hash = li.b64urlenc(li.hmac("sha256", secret, protected_path))
if (not li.digest_eq(hash, mac)) then
return 403 -- 403 Forbidden
end
-- remap to alternative filesystem path
local doc_root = "/var/www/servers/www.example.org/download"
req_attr["physical.doc_root"] = docroot
req_attr["physical.basedir"] = docroot
req_attr["physical.path"] = docroot .. rel_path
req_attr["physical.rel-path"] = rel_path
-- let mod_staticfile or other module handle the file
</pre>
h4. lua mod_setenv
<pre>
-- examples
local r = lighty.r
r.req_header["DNT"] = "1"
r.req_env["TMOUT"] = "3"
r.resp_header["Cache-Control"] = "max-age=0"
</pre>
h4. lua mod_simple_vhost
<pre>
local server_root = "/var/www/servers/"
local docroot = nil
local req_attr = lighty.r.req_attr
local host = string.match(req_attr["uri.authority"], "^(%w[^:/]*)")
if (host) then
docroot = server_root .. host
local st = lighty.c.stat(docroot)
if (not st or not st.is_dir) then
docroot = nil
end
end
if (not docroot) then -- set default docroot
docroot = server_root .. "default.example.com"
end
req_attr["physical.doc_root"] = docroot
req_attr["physical.basedir"] = docroot
req_attr["physical.path"] = docroot .. req_attr["physical.rel-path"]
</pre>
h4. lua mod_ssi
<pre>
-- construct page from one or more components (strings and/or files)
r.resp_body:set({ 'Hello!\n', { filename = '/path/to/main/page.inc' } })
return 200
</pre>
h4. lua mod_staticfile
<pre>
-- lighttpd 1.4.64+
local r = lighty.r
local path = r.req_attr["physical.path"]
local st = lighty.c.stat(path)
if (st and st.is_file) then
return st["http-response-send-file"]
end
</pre>
<pre>
-- lighttpd <= 1.4.63
-- (mod_staticfile handles HTTP conditional requests; not handled here)
--local r = lighty.r
--if (r.req_header["If-None-Match"] or r.req_header["If-Modified-Since"]) then
-- return 0
--end
local r = lighty.r
local path = r.req_attr["physical.path"]
local st = lighty.c.stat(path)
if (st and st.is_file) then
r.resp_header["Content-Type"] = st["content-type"]
--r.resp_header["ETag"] = st.etag
--r.resp_header["Last-Modified"] = os.date("!%a, %d %b %Y %T GMT",st.st_mtime)
r.resp_body:set({ { filename = path } })
return 200
end
</pre>
h4. lua mod_userdir
<pre>
local req_attr = lighty.r.req_attr
local user, relpath = string.match(req_attr["uri.path"], "^/~([^/]+)(/.*)?$")
if (user) then
local basedir = "/u/" .. user .. "/web"
req_attr["physical.basedir"] = basedir
req_attr["physical.path"] = basedir .. relpath
end
</pre>
h4. lua mod_usertrack
<pre>
local cookies = lighty.c.cookie_tokens(lighty.r.req_header['Cookie'])
if (cookies["TRACKID"]) then return 0 end -- cookie already exists
-- create cookie
local li = lighty.c
local md = li.md("sha256", lighty.r.req_attr["uri.path"]
.. "+" .. tostring(li.time()) .. tostring(li.rand()))
local usertrack_cookie =
"TRACKID=" .. md .. "; Path=/; Version=1; Domain=example.com; max-age=86400"
local resp_header = lighty.r.resp_header
local set_cookie = resp_header["Set-Cookie"]
if (set_cookie) then set_cookie = set_cookie .. "\r\nSet-Cookie: " end
resp_header["Set-Cookie"] = set_cookie .. usertrack_cookie
</pre>
h3. lua examples of deprecated lighttpd modules
Potential alternative reimplementations for deprecated lighttpd modules
h4. lua mod_flv_streaming
<pre>
$HTTP["url"] =~ "\.flv$" {
magnet.attract-physical-path-to = ("/path/to/flv-streaming.lua")
}
</pre>
<pre>
-- lua implementation of lighttpd mod_flv_streaming
--
-- Aside: it is baffling why FLV streaming is not simply an HTTP Range request
--
-- Potential (trivial) enhancements (exercise left to the reader):
-- - could call lighty.stat(lighty.env["physical.path"])
-- - check that file exists
-- - check start/end offsets
local r = lighty.r
-- check that target file ends in ".flv"
-- (comment out; already checked in lighttpd.conf condition in sample conf)
--if (".flv" ~= string.sub(r.req_attr["physical.path"], -4)) then
-- return 0
--end
-- split the query-string and look for start and end offsets (0-based)
local qs = lighty.c.urldec_query(r.req_attr["query-string"])
if (qs["start"] == nil) then
return 0
end
local start = tonumber(qs["start"])
local len = qs["end"] ~= nil and (tonumber(qs["end"]) + 1 - start) or -1
if (start <= 0) then -- let other modules handle request (e.g. mod_staticfile)
return 0
end
-- send FLV response
r.resp_header["Content-Type"] = "video/x-flv"
r.resp_body:set({
"FLV\1\1\0\0\0\9\0\0\0\9", -- FLV header
{ filename = r.req_attr["physical.path"], offset = start, length = len }
})
return 200
</pre>
h4. lua mod_trigger_b4_dl
<pre>
-- mod_trigger_b4_dl using memcached (untested; YMMV)
--
-- Dependency:
-- - https://luarocks.org/modules/akorn/lua-libmemcached
-- https://github.com/akornatskyy/lua-libmemcached
--
-- The problem that lighttpd mod_trigger_b4_dl originally aimed to solve is to
-- thwart deep linking. (Additional heuristics could be added to check Referer
-- request header.)
-- define memcached servers and configuration options (REVIEW/CHANGE THIS)
_G.config_options = '--server=127.0.0.1:11211'
-- '--server=127.0.0.1:11211 --namespace=...'
_G.trigger_pattern = '^/trigger/'
_G.download_pattern = '^/download/'
_G.deny_url = '/denied.html'
_G.expire = 10
-- retrieve libmemcached client configuration from (script-scoped) global
local mc = _G.mc
if (not mc) then
_G.libmemcached = require 'libmemcached'
-- use string identity for value since setting fixed value
_G.ident = function(s) return s end
_G.encoder = {encode = _G.ident, decode = _G.ident}
-- https://luarocks.org/modules/fperrad/lua-messagepack
-- _G.mp = require 'MessagePack'
-- _G.encoder = {encode = mp.pack, decode = mp.unpack}
_G.key_encode = function(s) return string.gsub(s, ' ', '-') end
_G.mc = libmemcached.new(_G.config_options, _G.encoder, _G.key_encode)
mc = _G.mc
end
local r = lighty.r
local remote_ip = nil
--
-- using X-Forwarded-For is possibly insecure unless always known to be set
-- by trusted upstream reverse-proxy directing requests to this server
--
--remote_ip = r.req_header["X-Forwarded-For"]
if (not remote_ip) then remote_ip = r.req_attr["request.remote-addr"] end
local key = remote_ip
if (string.match(lighty.env["uri.path"], _G.trigger_pattern)) then
if (not mc:set(key, '1', _G.expire)) then
print("memcached insert failed")
end
elseif (string.match(lighty.env["uri.path"], _G.download_pattern)) then
-- check that key exists
if (not mc:exist(key)) then
r.resp_header["Location"] = _G.deny_url
return 307
end
-- update timeout
if (not mc:set(key, '1', _G.expire)) then
print("memcached insert failed")
end
end
return 0
</pre>
<pre>
-- mod_trigger_b4_dl using gdbm (untested; YMMV)
--
-- (separate: admin should periodically prune expired entries from gdbm
-- and should gdbm_reorganize() gdbm to clean up space)
--
-- Dependency:
-- - http://luarocks.org/modules/luarocks/lgdbm
-- https://pjb.com.au/comp/lua/lgdbm.html
--
-- The problem that lighttpd mod_trigger_b4_dl originally aimed to solve is to
-- thwart deep linking. (Additional heuristics could be added to check Referer
-- request header.)
-- define memcached servers and configuration options (REVIEW/CHANGE THIS)
_G.gdbm_file
_G.trigger_pattern = '^/trigger/'
_G.download_pattern = '^/download/'
_G.deny_url = '/denied.html'
_G.expire = 10
-- retrieve open gdbm database handle from (script-scoped) global
local dbh = _G.dbh
if (not mc) then
_G.gdbm = require "gdbm"
_G.dbh = assert(_G.gdbm.open(_G.gdbm_file, "c"))
dbh = _G.dbh
end
local r = lighty.r
local remote_ip = nil
--
-- using X-Forwarded-For is possibly insecure unless always known to be set
-- by trusted upstream reverse-proxy directing requests to this server
--
--remote_ip = r.req_header["X-Forwarded-For"]
if (not remote_ip) then remote_ip = r.req_attr["request.remote-addr"] end
local key = remote_ip
local cur_ts = lighty.c.time()
if (string.match(lighty.env["uri.path"], _G.trigger_pattern)) then
if (not dbh:insert(key, cur_ts)) then
if (not dbh:replace(key, cur_ts)) then
print("gdbm insert failed")
end
end
elseif (string.match(lighty.env["uri.path"], _G.download_pattern)) then
-- check that key exists and entry has not expired
local last_hit = dbh:fetch(key)
if (not last_hit or cur_ts - last_hit > _G.expire) then
dbh:delete(key)
r.resp_header["Location"] = _G.deny_url
return 307
end
-- update timeout
if (not dbh:replace(key, cur_ts)) then
print("gdbm replace failed")
end
end
return 0
</pre>