Project

General

Profile

ModMagnetExamples » History » Revision 15

Revision 14 (gstrauss, 2021-12-12 19:06) → Revision 15/32 (gstrauss, 2022-02-17 03:48)

{{>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)) 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) 
 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["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>