Browse Source

[lua]: core and secdownload plugin

personal/stbuehler/wip
Stefan Bühler 12 years ago
parent
commit
ed58f7365f
  1. 109
      doc/core.lua
  2. 13
      doc/core__cached_html.lua
  3. 69
      doc/core__xsendfile.lua
  4. 57
      doc/secdownload.lua
  5. 52
      doc/secdownload__secdownload.lua

109
doc/core.lua

@ -0,0 +1,109 @@
-- contrib.lua
-- usage:
-- load mod_lua and this plugin:
-- setup {
-- module_load "mod_lua";
-- lua.plugin "/path/contrib.lua";
-- }
local filename, args = ...
-- basepath for loading sub handlers with lua.handler
-- this allows us to have lua actions that don't use the global lock
local basepath = filename:gsub("(.*/)(.*)", "%1")
-- core.wsgi:
-- WSGI applications expect the url to be split into SCRIPT_NAME and
-- PATH_INFO; SCRIPT_NAME is their "application root", and PATH_INFO the requsted
-- resource in the application.
-- By default, lighttpd uses an empty PATH_INFO (unless you used the "pathinfo;" action,
-- but this doesn't help here as we're not dealing with static files here)
-- Important: WSGI is an "extension" of CGI; it doesn't specify a transport protocol,
-- you can use it with plain CGI, FastCGI or SCGI (or anything else that supports
-- the basic CGI protocol)
--
-- Usage:
-- core.wsgi ( <url-prefix>, ${ <backend-actions>; } );
--
-- Example: Trac in "/trac", listening via FastCGI on unix:/tmp/trac.socket
-- You oviously have to load mod_fastcgi for this.
--
-- core.wsgi ( "/trac", ${ fastcgi "unix:/tmp/trac.socket"; } );
local function wsgi(uri_prefix, act)
if type(uri_prefix) ~= "string" then
lighty.error("wsgi expects a string (uri-prefix) as first parameter")
return nil
end
local uri_len = uri_prefix:len()
local function wsgi_rewrite(vr)
vr.phys.pathinfo = vr.req.uri.path:sub(uri_len)
vr.req.uri.path = uri_prefix
end
return action.when(request.path:prefix(uri_prefix),
action.list(wsgi_rewrite, act)
)
end
-- try to find a file for the current url with ".html" prefix,
-- if url doesn't already belong to a file and has not already ".html" prefix
-- example:
-- core.cached_html;
local function cached_html()
local try_cached_html = action.lua.handler(basepath .. 'core__cached_html.lua')
return action.when(physical.is_file:isnot(), action.when(physical.path:notsuffix('.html'), try_cached_html))
end
-- Usage
-- auth.require_user (userlist);
-- Require a specific authenticated user; put it after an auth action.
-- Be careful: the empty username matches unauthenticated users.
-- Example:
-- auth.plain [ "method": "basic", "realm": "test", "file": "test.plain" ];
-- auth.require_user ("foo1", "foo2");
local function auth_require_user(...)
local users = {...}
-- lighty.debug("auth_require_user")
if #users == 0 then
lighty.error("Empty userlist in auth.require_user")
return nil
else
local escapedusers = {}
for i, u in ipairs(users) do
escapedusers[i] = u:gsub("[]|+*[\\^$-]", "\\%0")
end
local regex = "^(" .. table.concat(escapedusers, "|") .. ")$"
-- lighty.debug("auth_require_user REMOTE_USER regex: " .. regex)
return action.when(request.environment["REMOTE_USER"]:nomatch(regex), action.auth.deny())
end
end
-- Usage:
-- core.xsendfile docroot;
-- The parameter is the doc-root the files has to be in (can be omitted)
-- Example:
-- core.xsendfile "/srv/";
local function xsendfile(docroot)
if docroot and type(docroot) =~ "string" then
lighty.error("xsendfile: parameter has to be a string")
return nil
end
local handle_x_sendfile = action.lua.handler(basepath .. 'core__xsendfile.lua', nil, docroot)
return handle_x_sendfile
end
actions = {
["core.wsgi"] = wsgi,
["core.cached_html"] = cached_html,
["auth.require_user"] = auth_require_user,
["core.xsendfile"] = xsendfile
}

13
doc/core__cached_html.lua

@ -0,0 +1,13 @@
local function try_cached_html(vr)
local p = vr.phys.path .. '.html'
st, res = vr:stat(p)
if st and st.is_file then
-- found the file
vr.phys.path = p
elseif res == lighty.HANDLER_WAIT_FOR_EVENT then
return lighty.HANDLER_WAIT_FOR_EVENT
end
-- ignore other errors
end
actions = try_cached_html

69
doc/core__xsendfile.lua

@ -0,0 +1,69 @@
local filename, args = ...
local docroot = args
-- create new class XFilterDrop
local XFilterDrop = { }
XFilterDrop.__index = XFilterDrop
-- "classmethod" to create new instance
function XFilterDrop:new(vr)
-- vr:debug("New XSendfile instance")
local o = { }
setmetatable(o, self)
return o
end
-- normal method to handle content
function XFilterDrop:handle(vr, outq, inq)
-- drop further input (we closed it already)
inq:skip_all()
return lighty.HANDLER_GO_ON
end
-- create a new filter which drops all input and adds it to the vrequest
-- returns the filter object so you can insert your own content in f.out (it is already closed)
local function add_drop_filter(vr)
local f = vr:add_filter_out(XFilterDrop:new())
f['in'].is_closed = true
f['in']:skip_all()
f.out.is_closed = true
return f
end
local function handle_x_sendfile(vr)
-- vr:debug("handle x-sendfile")
-- wait for response
if not vr.has_response then
if vr.is_handled then
-- vr:debug("x-sendfile: waiting for response headers")
return lighty.HANDLER_WAIT_FOR_EVENT
else
-- vr:debug("No response handler yet, cannot handle X-Sendfile")
return lighty.HANDLER_GO_ON
end
end
-- vr:debug("handle x-sendfile: headers available")
-- add filter if x-sendfile header is not empty
local xs = vr.resp.headers["X-Sendfile"]
if xs and xs ~= "" then
xs = lighty.path_simplify(xs)
if docroot and xs:sub(docroot.len()) =~ docroot then
vr:error("x-sendfile: File '".. xs .. "'not in required docroot '" .. docroot .. "'")
return lighty.HANDLER_GO_ON
end
-- make sure to drop all other content from the backend
local f = add_drop_filter(vr)
vr.resp.headers["X-Sendfile"] = nil -- remove header from response
-- Add checks for the pathname here
vr:debug("XSendfile:handle: pushing file '" .. xs .. "' as content")
f.out:add({ filename = xs })
end
end
actions = handle_x_sendfile

57
doc/secdownload.lua

@ -0,0 +1,57 @@
-- secdownload.lua
-- usage:
-- a) load mod_lua and this plugin:
-- setup {
-- module_load "mod_lua";
-- lua.plugin "/path/secdownload.lua";
-- }
-- b) activate it anywhere you want:
-- secdownload [ "prefix": "/sec/", "document-root": "/secret/path", "secret": "abc", "timeout": 600 ];
local filename, args = ...
-- basepath for loading sub handlers with lua.handler
-- this allows us to have lua actions that don't use the global lock
local basepath = filename:gsub("(.*/)(.*)", "%1")
local function secdownload(options)
if type(options) ~= "table" then
lighty.error("secdownload expects a hashtable as parameter")
return nil
end
local uri_prefix = options["prefix"]
local doc_root = options["document-root"]
local secret = options["secret"]
local timeout = tonumber(options["timeout"]) or 60
if secret == nil then
lighty.error("secdownload: need secret in options")
return nil
end
if doc_root == nil then
lighty.error("secdownload: need doc_root in options")
return nil
end
if doc_root:sub(-1) ~= "/" then
doc_root = doc_root .. "/"
end
if uri_prefix == nil then
uri_prefix = "/"
end
local args = { ["prefix"] = uri_prefix, ["document-root"] = doc_root, ["secret"] = secret, ["timeout"] = timeout }
local handle_secdownload = action.lua.handler(basepath .. 'core__cached_html.lua', nil, args)
return handle_secdownload
end
actions = {
["secdownload"] = secdownload
}

52
doc/secdownload__secdownload.lua

@ -0,0 +1,52 @@
local filename, args = ...
local uri_prefix = args["prefix"]
local doc_root = args["document-root"]
local secret = args["secret"]
local timeout = tonumber(args["timeout"])
local function deny_access(vr, status)
vr:handle_direct()
vr.resp.status = status
end
local function handle_secdownload(vr)
local path = vr.req.uri.path
local prefix_len = uri_prefix:len()
if path:sub(1, prefix_len) == uri_prefix and not vr.is_handled then
local md5str = path:sub(prefix_len + 1, prefix_len + 32)
if md5str:len() ~= 32 then return deny_access(vr, 403) end
if path:sub(prefix_len + 33, prefix_len + 33) ~= "/" then return deny_access(vr, 403) end
local slash = path:find("/", prefix_len + 34)
if slash == nil then return deny_access(vr, 403) end
local ts_string = path:sub(prefix_len + 34, slash - 1)
local timestamp = tonumber(ts_string, 16)
if timestamp == nil then return deny_access(vr, 403) end
path = path:sub(slash)
local ts = os.time()
if (timestamp < ts - timeout) or (timestamp > ts + timeout) then
-- Gone, not Timeout (don't retry later)
return deny_access(vr, 410)
end
-- modify md5content as you wish :)
local md5content = secret .. path .. ts_string
-- vr:debug("checking md5: '" .. md5content .. "', md5: " .. md5str)
if md5str ~= lighty.md5(secret .. path .. ts_string) then
return deny_access(vr, 403)
end
-- rewrite physical paths
vr.phys.doc_root = doc_root
vr.phys.path = doc_root .. path
end
end
actions = handle_secdownload
Loading…
Cancel
Save