Actions
ModMagnetExamples » History » Revision 22
« Previous |
Revision 22/32
(diff)
| Next »
gstrauss, 2022-05-09 07:50
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 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 i, ireq_attr, ireq_item, iresp_body, ireq_body for i in server.irequests() do ireq_attr = i.req_attr ireq_item = i.req_item ireq_body = i.req_body iresp_body= i.resp_body 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_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¶
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) 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 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
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 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"] 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 over 2 years ago · 22 revisions