ModMagnetExamples » History » Revision 17
Revision 16 (gstrauss, 2022-02-17 03:52) → Revision 17/32 (gstrauss, 2022-04-27 08:30)
{{>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; charset=utf-8" 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 end </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) local r = lighty.r r.resp_header["Content-Type"] = "text/plain; charset=utf-8" 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["uri.query"]) 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_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_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> 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>