Project

General

Profile

ModMagnetExamples » History » Revision 24

Revision 23 (gstrauss, 2022-05-09 23:46) → Revision 24/27 (gstrauss, 2022-05-10 03:17)

{{>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 
     r.req_item.keep_alive = -1    -- (feature added in lighttpd 1.4.65+) 
     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_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_status 

 <pre> 
 -- lua mod_status 
 -- (requires lighttpd 1.4.65+) 
 -- 
 -- This sample script provides most of the information provided by mod_status. 
 -- (Some server-wide metrics collected by mod_status are not available here.) 
 -- (Use mod_status /status?json or /status?jsonp to obtain server-wide metrics.) 
 -- 
 -- Output format differs from the HTML of mod_status only since HTML would add 
 -- noise to this sample script and obscure the simple mechanisms to obtain 
 -- status data.    (The below may be adjusted slightly to be full JSON output.) 

 local r = lighty.r 
 r.resp_header["Content-Type"] = "text/plain" 

 -- server-wide info 
 local server = lighty.server 
 local uptime = server.stats.uptime 
 r.resp_body.add({ 
   'lighttpd server status', 
   '\nstarted: ', os.date("![%FT%TZ]", lighty.c.time() - uptime), 
   '\nuptime: ', uptime, 
   '\nconnection open: ', server.stats.clients_open, 
   '\nrequests:\n' 
 }) 

 -- request info 
 -- Information about each request, not each connection.    With HTTP/2, multiple 
 -- streams may be open in parallel and are reported as individual requests 
 -- following the HTTP/2 control stream (reported with "PRI" method and URL "*"). 
 -- (Each HTTP/1.x connection reported below as request in "keep-alive" stage is 
 --    waiting for next request, whereas HTTP/2 connections without any open 
 --    streams have control stream "PRI *" remain in the "write" state.) 

 -- create table to use as template (and for reuse) 
 -- minimize string copying due to concatentation 
 local t = {nil,nil,nil,nil,nil,nil,nil,nil,nil,nil, 
            nil,nil,nil,nil,nil,nil,nil,nil,nil,nil, 
            nil,nil,nil,nil,nil} 
 -- 
   t[1] =      'Client Addr: "' 
   t[2] = nil -- ireq_attr["request.remote-addr"] 
   t[3] = '", Read: "' 
   t[4] = nil -- ireq_body.bytes_in 
   t[5] = '/' 
   t[6] = nil -- ireq_body.len 
   t[7] = '", Written: "' 
   t[8] = nil -- iresp_body.bytes_out 
   t[9] = '/' 
   t[10]= nil -- iresp_body.bytes_in 
   t[11]= '", State: "' 
   t[12]= nil -- ireq_attr["request.stage"] 
   t[13]= '", Time: ' 
   t[14]= nil -- ireq_item.start_time 
   t[15]= ', Host: "' 
   t[16]= nil -- stresc(ireq_attr["request.server-name"]) 
   t[17]= '", ', Method: "' 
   t[18]= nil -- ireq_attr["request.method"] 
   t[19]= '", URI: "' 
   t[20]= nil -- stresc(ireq_attr["request.uri"]) 
   t[21]= ' (' 
   t[22]= nil -- stresc(ireq_attr["request.orig-uri"]) 
   t[23]= ')", File: "' 
   t[24]= nil -- stresc(ireq_attr["physical.path"]) 
   t[25]= '"\n' 
 -- 

 local stresc = lighty.c.bsenc_json 
 local nrequests = 0 
 local i, ireq_attr, ireq_item, iresp_body, ireq_body 
 for i in server.irequests() do 
   if (nrequests == 0) then 
     -- request object is reused each iteration 
     ireq_attr = i.req_attr 
     
   ireq_item = i.req_item 
     
   ireq_body = i.req_body 
     
   iresp_body= i.resp_body 
   end 
   nrequests = nrequests + 1 

   t[2] = ireq_attr["request.remote-addr"] 
   t[4] = (ireq_item.stream_id == 0 and ireq_body.len == -1 
           and ireq_attr["request.protocol"] == "HTTP/2.0") 
          and ireq_item.bytes_in -- HTTP/2 control stream id 0 bytes in 
           or ireq_body.bytes_in -- request body bytes in 
   t[6] = ireq_body.len 
   t[8] = iresp_body.bytes_out 
   t[10]= iresp_body.bytes_in 
   t[12]= ireq_attr["request.stage"] 
   t[14]= ireq_item.start_time() 
   t[16]= stresc(ireq_attr["request.server-name"]) 
   t[18]= ireq_attr["request.method"] 
   t[20]= stresc(ireq_attr["request.uri"]) 
   t[22]= stresc(ireq_attr["request.orig-uri"]) 
   t[24]= stresc(ireq_attr["physical.path"]) 

   r.resp_body.add(t) 

   -- (reusing table template above is faster than below, 
   --    which constructs new table for each request) 
   --r.resp_body.add({ 
   --    'Client Addr: "', ireq_attr["request.remote-addr"], '", ', 
   --    'Read: "', ireq_body.bytes_in,'/',ireq_body.len, '", ', 
   --    'Written: "', iresp_body.bytes_out,'/',iresp_body.bytes_in, '", ', 
   --    'State: "', ireq_attr["request.stage"], '", ', 
   --    'Time: ', ireq_item.start_time(), ', ', 
   --    'Host: "', stresc(ireq_attr["request.server-name"]), '", ', 
   --    'Method: "', ireq_attr["request.method"]), '", ', 
   --    'URI: "', stresc(ireq_attr["request.uri"]), 
   --        ' (', stresc(ireq_attr["request.orig-uri"]), ')", ', 
   --    'File: "', stresc(ireq_attr["physical.path"]), '"\n' 
   --}) 
 end 

 return 200 
 </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> 


 h3. lua examples of deprecated lighttpd modules 

 Potential alternative reimplementations for deprecated lighttpd modules 


 h4. lua mod_evasive 

 <pre> 
 -- lua mod_evasive 
 -- (requires lighttpd 1.4.65+) 

 local addr = lighty.r.req_attr["request.remote-addr"] 

 -- count number of requests matching remote address (including self) 
 -- and for which request headers have been received.    This could be 
 -- extended to count number of requests matching a given path prefix, 
 -- which is recommended since multiple request objects may be returned 
 -- on an HTTP/2 connection with multiple active streams.    The code below 
 -- checks IP-port combination to count HTTP/2 connections only once. 
 -- (note: port always 0 if listening unix domain sockets; adjust accordingly) 
 -- (Alternative implementation could omit port check and instead check 
 --      (i.req_item.stream_id == 0)    -- HTTP/1.x or HTTP/2 control stream 
 --    if interested in connection count only, instead of matching a path) 
 local ireq_attr, count, prev, port = nil, 0, 0, 0 
 for i in lighty.server.irequests() do 
   ireq_attr = i.req_attr 
   if (ireq_attr["request.remote-addr"] == addr 
       and ireq_attr["uri.path"] ~= nil) then 
     port = ireq_attr["request.remote-port"] 
     if (prev ~= port) then 
       prev = port 
       -- save desired info; i invalid outside lighty.server.irequests iteration 
       count = count + 1 
       if (count > 1) then break end 
     end 
   end 
 end 

 -- check if count exceeds policy 
 -- (adjust count limit to your site needs here and above) 
 if (count > 1) then 
   -- (optional) logging 
   --print(addr .. ' turned away. Too many connections.') 
   -- 403 Forbidden 
   return 403 
   -- Alternatives: 
   -- could set Location response header to alternate site and send 302 redirect 
   -- could rewrite the request to a static page for /site-busy.html 
   --     (and be sure to also disable caching, e.g. Cache-Control: max-age=0) 
 end 

 -- Limitations: (lighttpd mod_evasive had the same limitations.) 
 -- The above lua scans all active requests for matching remote address, but only 
 -- sees the current lighttpd process.    This may miss requests when there are 
 -- multiple lighttpd workers or independent servers. 
 -- This lua code rejects requests, but does not close connections, so this can 
 -- be used to limit the number of simultaneous accesses to resource-intensive 
 -- targets, but this is not very effective to limit number of socket connections 
 -- from the same IP. 

 -- General notes: countermeasures should counter some undesirable behavior and 
 -- countermeasures should be selected based on the undesirable behavior. 
 -- As mentioned above, lighttpd lua mod_evasive can limit number of parallel 
 -- requests being serviced for a given set of resources, and the limit can be 
 -- removed or different for a whitelist of remote addresses.    lighttpd.conf 
 -- conditionals can set connection.kbytes-per-second to limit bandwidth 
 -- throughput per connection.    Host firewall or lighttpd mod_access or lua 
 -- can be used to block remote addresses/networks from accessing server 
 -- resources.    Additional countermeasures can be implemented in lua, too: 
 --     https://wiki.lighttpd.net/AbsoLUAtion#Fight-DDoS 
 --     https://wiki.lighttpd.net/AbsoLUAtion#Mod_Security 
 </pre> 


 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"]) 
 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_uploadprogress 

 <pre> 
 -- lua mod_uploadprogress 
 -- (requires lighttpd 1.4.65+) 

 local r = lighty.r 
 local req_attr = r.req_attr 
 local req_header = r.req_header 

 function get_id() 
   return req_header["X-Progress-ID"] 
     or (lighty.c.urldec_query(req_attr["uri.query"]))["X-Progress-ID"] 
     or string.match(req_attr["uri.path"], "/(%x+)$") 
 end 

 -- POST: place X-Progress-ID in request headers, if not already there 

 local method = req_attr["request.method"] 
 if (method == "POST") then 
   if (not req_header["X-Progress-ID"]) then 
     local id = get_id() 
     if (id ~= nil) then req_header["X-Progress-ID"] = id end 
   end 
   return 0 
 end 

 -- GET /progress: check method and uri path (or set your own custom path) 

 if (method ~= "GET") then return 0 end 
 if (not string.match(req_attr["uri.path"], "^/progress")) then return 0 end 
 local id = get_id() 
 -- mod_uploadprogress (historical behavior) returns if no id, 
 -- even though it is an improper request to /progress endpoint 
 -- (This is poor behavior and one reason why mod_uploadprogress is deprecated) 
 if (id == nil) then return 0 end 

 -- search for X-Progress-ID in existing POST requests 
 local i, sz, recvd 
 for i in lighty.server.irequests() do 
   if (i.req_attr["request.method"] == "POST" 
       and i.req_header["X-Progress-ID"] == id) then 
     -- save desired info; i is invalid outside lighty.server.irequests iteration 
     sz      = i.req_body["len"] 
     recvd = i.req_body["bytes_in"] 
     break 
   end 
 end 

 -- mod_uploadprogress (historical behavior) returns simple string if not found 
 -- (and does not set Content-Type, so is Content-Type: application/octet-stream) 
 -- (This is poor behavior and one reason why mod_uploadprogress is deprecated) 
 if (sz == nil) then 
   r.resp_body:set({"not in progress"}) 
   return 200 
 end 

 local resp_header = r.resp_header 
 resp_header["Content-Type"] = "text/xml" 
 resp_header["Pragma"] = "no-cache" 
 resp_header["Expires"] = "Thu, 19 Nov 1981 08:52:00 GMT" 
 resp_header["Cache-Control"] = 
   "no-store, no-cache, must-revalidate, post-check=0, pre-check=0" 
 r.resp_body:set({ 
   '<?xml version="1.0" encoding="iso-8859-1"?><upload><size>', 
   sz, '</size><received>', recvd, '</received></upload>' }) 
 return 200 

 -- Limitations: 
 -- The above lua scans all active requests for matching X-Progress-ID, but only 
 -- sees the current lighttpd process.    This may miss requests when there are 
 -- multiple lighttpd workers or independent servers. 
 -- A more robust implementation might consider memcached or redis to store 
 -- upload progress.    Backend processes receiving upload could update the 
 -- database periodically (e.g. after each xxxx bytes are received), and this 
 -- lua could be changed to access the database.    See lua mod_trigger_b4_dl for 
 -- a lighttpd lua example using memcached. 
 </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>