Project

General

Profile

Actions

ModMagnetExamples » History » Revision 29

« Previous | Revision 29/32 (diff) | Next »
gstrauss, 2024-04-12 16:24


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)

lua mod_access

-- 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

lua mod_alias

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

lua mod_auth

-- 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" 

lua mod_dirlisting

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

lua mod_evhost

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

lua mod_expire

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

lua mod_extforward

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

lua mod_indexfile

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

lua mod_redirect

-- 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

lua mod_rewrite

-- 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

lua mod_setenv

-- examples
local r = lighty.r
r.req_header["DNT"] = "1" 
r.req_env["TMOUT"] = "3" 
r.resp_header["Cache-Control"] = "max-age=0" 

lua mod_simple_vhost

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

lua mod_ssi

-- 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

lua mod_staticfile

-- 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
-- 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

lua mod_status

-- 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 sstats = server.stats
local uptime = sstats.uptime
r.resp_body.add({
   'version: ', sstats.version,
  '\nstarted: ', os.date("![%FT%TZ]", lighty.c.time() - uptime),
  '\nuptime: ', uptime,
  '\nconnection open: ', sstats.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

lua mod_unique_id

-- similar to Apache mod_unique_id (but not the same exact contents or format)
local r = lighty.r
local c = lighty.c
r.req_env["UNIQUE_ID"] = c.b64urlenc(tostring(c.time())
                                  .. r.req_attr["request.remote-addr"]
                                  .. tostring(c.rand()))
return 0
or
-- similar to UUIDv4 (all-random identifier, but not UUIDv4 format)
local c = lighty.c
lighty.r.req_env["UNIQUE_ID"] = c.rand() .. c.rand() .. c.rand() .. c.rand()
return 0

lua mod_userdir

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

lua examples of deprecated lighttpd modules

lua examples of retired lighttpd modules

Potential alternative reimplementations for deprecated lighttpd modules

lua mod_evasive

-- 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

lua mod_flv_streaming

$HTTP["url"] =~ "\.flv$" {
  magnet.attract-physical-path-to = ("/path/to/flv-streaming.lua")
}
-- 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

lua mod_secdownload

-- 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 docroot = "/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

lua mod_trigger_b4_dl

-- 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
-- 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

lua mod_uploadprogress

-- 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.

lua mod_usertrack

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

Updated by gstrauss 7 months ago · 29 revisions