AbsoLUAtion - Lighttpd + Lua 的强大组合¶
- 目录
- AbsoLUAtion - Lighttpd + Lua 的强大组合
- 要求
- 链接
- 代码片段
- 内容协商
- 对抗 DDoS
- Mod_Security
- 安全响应
- 首字节时间
- 客户端证书 HTTP 头字段
- 请求限制器
- 其他解决方案
- 示例文件
我们希望为 lighttpd + Lua 构建一个中心资源,因为这是 lighttpd 相对于其他 Web 服务器的最大优势之一。它有用、方便、简单,有时是一个非常强大的组合,能为您提供额外的灵活性,并提供其他 HTTP 服务器无法解决的大小问题的解决方案!
再次,我们希望您,lighttpd 的用户,通过贡献链接、代码片段,或者仅仅是提供您的 Lua 脚本并附带它们功能的小描述以及它们如何帮助 lighttpd 完成您想要其完成的任务来支持本页面。
要求¶
- lighttpd mod_magnet
- Lua (v5.1; lighttpd 1.4.40+ 也应支持 v5.2, v5.3, v5.4) https://lua.ac.cn
链接¶
- http://www.sitepoint.com/blogs/2007/04/10/faster-page-loads-bundle-your-css-and-javascript/
- WP-MultiUser http://www.bisente.com/blog/2007/04/08/lighttpd-wordpressmu-english/
- 动态生成缩略图并缓存 http://www.xarg.org/2010/04/dynamic-thumbnail-generation-on-the-static-server/
- Drupal/OpenAtrium 简洁 URL 解决方案 drupal-clean-url-lighttpd
- 通过 OpenID 等进行认证 https://github.com/chmduquesne/lighttpd-external-auth (博客文章)
死链接?您不希望在此处列出?请将其删除。谢谢!
代码片段¶
Apache .htaccess 替代方案
将 .htaccess 功能迁移到 lighttpd 的选项
Apache .htaccess 中的常见用法RewriteCond %{REQUEST_FILENAME} !-fRewriteCond %{REQUEST_FILENAME} !-dRewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
- url.rewrite-if-not-file (如果 !-f 足够则推荐;不检查 !-d)
- lua mod_rewrite (推荐;比下面的选项更简单)
- mod_magnet 无需重启请求即可重写路径(见下文)
由于 lighttpd 默认不提供 is_file/is_dir 检查,mod_magnet 再次派上用场。我从 darix 网站上获取了 Drupal 的示例。
假设 Drupal 已安装在 http://example.com/drupal/,您现在为其添加 magnet 部分。
$HTTP["url"] =~ "^/drupal" {
# we only need index.php here.
index-file.names = ( "index.php" )
# for clean urls
magnet.attract-physical-path-to = ( "/etc/lighttpd/drupal.lua" )
}
drupal.lua 文件
-- little helper function
function file_exists(path)
return lighty.stat(path) and true or false
end
function removePrefix(str, prefix)
return str:sub(1,#prefix+1) == prefix.."/" and str:sub(#prefix+2)
end
-- prefix without the trailing slash
local prefix = '/drupal'
-- the magic ;)
if (not file_exists(lighty.env["physical.path"])) then
-- file still missing. pass it to the fastcgi backend
request_uri = removePrefix(lighty.env["uri.path"], prefix)
if request_uri then
lighty.env["uri.path"] = prefix .. "/index.php"
local uriquery = lighty.env["uri.query"] or ""
lighty.env["uri.query"] = uriquery .. (uriquery ~= "" and "&" or "") .. "q=" .. request_uri
lighty.env["physical.rel-path"] = lighty.env["uri.path"]
lighty.env["request.orig-uri"] = lighty.env["request.uri"]
lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"]
end
end
-- fallthrough will put it back into the lighttpd request loop
-- that means we get the 304 handling for free. ;)
覆盖默认的 MIME 类型/内容类型
将 "magnet.attract-physical-path-to = ( "/path-to/change-ctype.lua" )" 添加到 lighttpd.conf 中,并将以下内容保存为 "change-ctype.lua"
if (string.match(lighty.env["physical.rel-path"], ".swf")) then
lighty.header["Content-Type"] = "text/html"
end
将文本文件作为 HTML 发送
这有点简单化,但它说明了这种想法:取一个文本文件并用 "< pre >" 标签包裹它。
配置文件
magnet.attract-physical-path-to = (server.docroot + "/readme.lua")
readme.lua
lighty.content = { "<pre>", { filename = "/README" }, "</pre>" }
lighty.header["Content-Type"] = "text/html"
return 200
重定向映射
redirect-map.lua 基于 URL 路径的高性能重定向映射
简单的维护脚本
您需要三个文件:maint.up、maint.down 和 maint.html。
maint.html 包含一个简单的 HTML 页面,用于在维护模式下向用户显示您想要的内容。
将 "magnet.attract-physical-path-to = ( "/path-to-your/maint.lua" )" 添加到您的 lighttpd.conf 中,最好是在全局部分或您的配置的主机部分中,例如您知道需要不时进行维护的论坛/维基。如果您想切换到维护模式,只需将 maint.down 复制到您的 "/path-to-your/" 位置的 maint.lua,lighttpd 就会向所有用户显示您的 maint.html - 无需重启任何东西 - 这可以即时完成。工作完成,一切恢复正常?将 maint.up 复制到您的 "/path-to-your/" 位置的 maint.lua。maint.up 在做什么?什么也没做,只是继续正常的文件服务 :-)
maint.up - 一切正常,用户将看到正常页面
-- This is empty, nothing to do.
maint.down - lighttpd 将显示维护页面 -> maint.html
-- lighty.header["X-Maintenance-Mode"] = "1"
-- uncomment the above if you want to add the header
lighty.content = { { filename = "/path-to-your/maint.html" } }
lighty.header["Content-Type"] = "text/html"
return 503
-- or return 200 if you want
自定义错误页面
mod_magnet 可用于实现 lighttpd 的任何或所有指令:`server.error-handler`、`server.error-handler-404`、`server.errorfile-prefix`、`server.error-intercept`
magnet.attract-response-start-to = ( "/absolute/path/to/script.lua" )
-- intercept all HTTP status errors (e.g. server.error-intercept = "enable")
local r = lighty.r
local http_status = r.req_item.http_status
if (http_status < 400) -- not an HTTP status error; do not modify
return 0
end
-- send back custom error page (similar to server.errorfile-prefix)
-- replace existing response body, if any
local errfile = "/path/to/errfiles/" .. http_status .. ".html"
local st = lighty.c.stat(errfile)
if (not st) then
errfile = "/path/to/errfiles/generic.html"
fi
r.resp_body:set({ { filename = errfile } })
-- (alternative: construct custom error page (similar to server.error-handler))
return http_status
从目录中选择一个随机文件
假设您想从目录中发送一个随机文件(广告内容)。
为了简化代码并提高性能,我们定义
- 所有图片都具有相同的格式(例如 image/png)
- 所有图片都使用从 1 开始递增的数字
- 一个特殊的索引文件命名了最高数字
配置
server.modules += ( "mod_magnet" )
magnet.attract-physical-path-to = ("random.lua")
random.lua
dir = lighty.env["physical.path"]
f = assert(io.open(dir .. "/index", "r"))
maxndx = f:read("*all")
f:close()
ndx = math.random(maxndx)
lighty.content = { { filename = dir .. "/" .. ndx }}
lighty.header["Content-Type"] = "image/png"
return 200
拒绝 URL 中的非法字符序列
您可能不想实现 mod_security,而只想对内容应用过滤器并拒绝看起来像 SQL 注入的特殊序列。
常见的注入是使用 UNION 将一个查询扩展为另一个 SELECT 查询。
if (string.find(lighty.env["request.uri"], "UNION%s")) then
return 400
end
流量配额
如果您只允许您的虚拟主机每月有一定的流量配额,并且希望在达到流量时禁用它们,这或许会有所帮助
host_blacklist = { ["www.example.org"] = 0 }
if (host_blacklist[lighty.request["Host"]]) then
return 404
end
只需按照所示方式将您想要列入黑名单的主机添加到黑名单表中。
复杂重写
如果您想在您的文档根目录上实现缓存,并且只希望在请求的文件不存在时才重新生成内容,您可以吸引 physical.path
magnet.attract-physical-path-to = ( server.document-root + "/rewrite.lua" )
rewrite.lua
attr = lighty.stat(lighty.env["physical.path"])
if (not attr) then
-- we couldn't stat() the file for some reason
-- let the backend generate it
lighty.env["uri.path"] = "/dispatch.fcgi"
lighty.env["physical.rel-path"] = lighty.env["uri.path"]
lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"]
end
扩展名重写
如果您想隐藏文件扩展名(例如 .php),您可以吸引 physical.path
magnet.attract-physical-path-to = ( server.document-root + "/rewrite.lua" )
rewrite.lua
attr = lighty.stat(lighty.env["physical.path"] .. ".php")
if (attr) then
lighty.env["uri.path"] = lighty.env["uri.path"] .. ".php"
lighty.env["physical.rel-path"] = lighty.env["uri.path"]
lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"]
end
用户跟踪
... 或者如何在脚本上下文中全局存储数据
每个脚本都有自己的脚本上下文。当脚本启动时,它只包含 Lua 函数和特殊的 lighty.* 命名空间。如果您想在脚本运行之间保存数据,可以使用全局脚本上下文
if (nil == _G["usertrack"]) then
_G["usertrack"] = {}
end
if (nil == _G["usertrack"][lighty.request["Cookie"]]) then
_G["usertrack"][lighty.request["Cookie"]]
else
_G["usertrack"][lighty.request["Cookie"]] = _G["usertrack"][lighty.request["Cookie"]] + 1
end
print _G["usertrack"][lighty.request["Cookie"]]
全局上下文是针对每个脚本的。如果您在不重启服务器的情况下更新脚本,上下文仍将保留。
WordpressMU
wpmu.lua
if (not lighty.stat(lighty.env["physical.path"])) then
if (string.match(lighty.env["uri.path"], "^(/?[^/]*/)files/$")) then
lighty.env["physical.rel-path"] = "index.php"
else
n, a = string.match(lighty.env["uri.path"], "^(/?[^/]*/)files/(.+)")
if a then
lighty.env["physical.rel-path"] = "wp-content/blogs.php"
lighty.env["uri.query"] = "file=" .. a
else
n, a = string.match(lighty.env["uri.path"], "^(/[^/]*)/(wp-.*)")
if a then
lighty.env["physical.rel-path"] = a;
else
n, a = string.match(lighty.env["uri.path"], "^(/[^/]*)/(.*\.php)$")
if a then
lighty.env["physical.rel-path"] = a
else
lighty.env["physical.rel-path"] = "index.php"
end
end
end
end
lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. "/".. lighty.env["physical.rel-path"]
end
内容协商¶
content-negotiation.lua 用于解析 Accept-Language 和 Accept-Encoding (#2678, #2736)以确定最佳目标文件
content-negotiation.lua
相关内容,请参阅 #1259 中的 Lua 代码,该代码尝试多种扩展名(类似于 Apache mod_autoext)以查找目标文件
对抗 DDoS¶
如果您的服务器因有人用请求洪泛而负载过高,一点点 Lua 可能会帮助您。;) 在我们的案例中,我们收到了许多请求头中没有 User-Agent 的请求。
if ( lighty.request["User-Agent"]== nil ) then
file = io.open ("ips.txt","a")
file:write(lighty.env["request.remote-ip"])
file:write("\n")
file:close()
return 200
end
request.remote-ip 字段自 lighttpd 1.4.23 版本起可用。ips.txt 文件必须对 lighttpd 用户(www-data)可写。ips.txt 文件中的恶意用户可以通过一个小脚本被丢弃到防火墙中。
Lua 也可以用于访问数据库,并根据数据库中的数据拒绝请求。
请参阅本页底部“文件”部分中附加的示例 reject-bad-actors.lua。它使用 mcdb 常量数据库进行快速查找。
Mod_Security¶
Apache 有 mod_security 作为 WAF(Web 应用程序防火墙)可用,但其他 Web 服务器没有。我编写了一个快速且粗糙的脚本,使用 mod_magnet 执行与 mod_security 类似的任务。
lighttpd-mod_security-via-mod_magnet
我最近一直在为 OpenResty(一个 nginx+luajit 组合)开发 libmodsecurity 绑定。使用修补的 libmodsecurity 和 cffi-lua,以及 modsec.lua 和 mod_magnet,lighttpd 可以执行传入请求检查。目前无法执行请求体(POST)和响应检查。最后两项将涉及 lighty 中的额外 Lua 入口点。安全规则可以在 coreruleset 获取。
lighttpd 的 waf.lua
local modsec = require "modsec"
local ok, err = modsec.init("/etc/owasp/modsec.conf")
if not ok then
print(err)
return
end
local transaction = modsec.transaction()
if not transaction then
print("Failed to initialize transaction")
end
-- evaluate connection info and request headers
local req_attr = lighty.r.req_attr
local url = req_attr["uri.scheme"]
.. "://"
.. req_attr["uri.authority"]
.. req_attr["uri.path-raw"]
.. (req_attr["uri.query"] and ("?" .. req_attr["uri.query"]) or "")
local res, err = transaction:eval_connection(req_attr["request.remote-addr"],req_attr["request.remote-port"],
req_attr["uri.authority"],req_attr["request.server-port"],url,
req_attr["request.method"],req_attr["request.protocol"])
if err then
print("Failed to evaluate connection: ",err)
end
local res, err = transaction:eval_request_headers(lighty.r.req_header)
if err then
print("Failed to evaluate request headers: ",err)
end
--[[ evaluate request body
Currently no way to evaluate request body
but this function must be run even with nil as arguments
]]
local res, err = transaction:eval_request_body(nil,nil)
if err then
print("Failed to evaluate request body: ",err)
end
-- Here decision could be made upon modsecurity variables whether handle this request or not
local score = tonumber(transaction.var.tx.anomaly_score)
if score >= 8 then
print("This request looks nasty overall score is: "..score)
return 403
end
owasp/modsec.conf 示例
#This is libmodsecurity base configuration Include modsecurity.conf Include /opt/openresty/owasp/crs-setup.conf Include /opt/openresty/owasp/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf Include /opt/openresty/owasp/rules/REQUEST-901-INITIALIZATION.conf Include /opt/openresty/owasp/rules/REQUEST-905-COMMON-EXCEPTIONS.conf Include /opt/openresty/owasp/rules/REQUEST-910-IP-REPUTATION.conf Include /opt/openresty/owasp/rules/REQUEST-911-METHOD-ENFORCEMENT.conf Include /opt/openresty/owasp/rules/REQUEST-912-DOS-PROTECTION.conf Include /opt/openresty/owasp/rules/REQUEST-913-SCANNER-DETECTION.conf Include /opt/openresty/owasp/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf Include /opt/openresty/owasp/rules/REQUEST-921-PROTOCOL-ATTACK.conf Include /opt/openresty/owasp/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf Include /opt/openresty/owasp/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf Include /opt/openresty/owasp/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf Include /opt/openresty/owasp/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf Include /opt/openresty/owasp/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf Include /opt/openresty/owasp/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf Include /opt/openresty/owasp/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf Include /opt/openresty/owasp/rules/REQUEST-949-BLOCKING-EVALUATION.conf Include /opt/openresty/owasp/rules/RESPONSE-950-DATA-LEAKAGES.conf Include /opt/openresty/owasp/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf Include /opt/openresty/owasp/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf Include /opt/openresty/owasp/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf Include /opt/openresty/owasp/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf Include /opt/openresty/owasp/rules/RESPONSE-959-BLOCKING-EVALUATION.conf Include /opt/openresty/owasp/rules/RESPONSE-980-CORRELATION.conf Include /opt/openresty/owasp/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
安全响应¶
Lua 脚本可以快速部署作为安全响应,以减轻针对 CGI 和其他后端漏洞的攻击。
-- reject request if request-line URL or request headers contain literal '{'
--
-- If not expecting literal '{' in HTTP requests, then this heuristic to
-- reject '{' thwarts certain classes of attacks on servers behind lighttpd.
-- - Log4Shell CVE-2021-44228 (and related) (Apache log4j RCE)
-- - Shellshock CVE-2014-6271 (and related) (Bash env RCE)
--
local r = lighty.r
-- check url-path (url-decoded) for literal '{'
if (string.find(r.req_attr["uri.path"], "{")) then
return 403
end
-- check query-string (url-encoded) for literal '{'
local q = r.req_attr["uri.query"]
if q and (string.find(q, "{")) then
return 403
end
-- check request headers for literal '{'
local req_header = r.req_header
for _, v in pairs(req_header) do
if (string.find(v, "{")) then
return 403
end
end
return 0
首字节时间¶
-- Time-to-First-Byte (TTFB)
-- (requires lighttpd 1.4.65+)
function elapsed_time(r)
local start_sec, start_nsec = r.req_item.start_time()
local elapsed_sec, elapsed_nsec = lighty.c.hrtime()
elapsed_sec = elapsed_sec - start_sec
elapsed_nsec = elapsed_nsec - start_nsec
if (elapsed_nsec < 0) then
elapsed_nsec = elapsed_nsec + 1000000000
elapsed_sec = elapsed_sec - 1
end
return elapsed_sec, elapsed_nsec
end
-- save time-to-first-byte (ttfb) time in env for later use in logging
-- (e.g. accesslog.format = "%h %V %u %t \"%r\" %>s %b %{ttfb}e %D")
local r = lighty.r
local elapsed_sec, elapsed_nsec = elapsed_time(r)
r.req_env["ttfb"] = math.floor(elapsed_sec*1000000 + elapsed_nsec/1000) -- usecs
-- (might use math.tointeger() in lua 5.3+ instead of math.floor())
客户端证书 HTTP 头字段¶
-- Client-Cert HTTP Header Field -- -- https://github.com/httpwg/http-extensions/blob/main/draft-ietf-httpbis-client-cert-field.md -- https://www.ietf.org/archive/id/draft-ietf-httpbis-client-cert-field-01.html -- -- (note: Client-Cert-Chain not implemented in lighttpd) -- -- This code intended for use with TLS client certificate verification -- for requests forwarded by lighttpd mod_proxy to other HTTP backends -- ssl.verifyclient.activate = "enable" -- ssl.verifyclient.exportcert = "enable" -- convert CGI environment variable SSL_CLIENT_CERT -- to structured field Client-Cert request header local r = lighty.r local cert = r.req_env["SSL_CLIENT_CERT"] if (cert) then cert = string.gsub(string.gsub(cert, "[-][^\n]+[-]", ":"), "%s", "") end -- set Client-Cert request header if lighttpd verified client certificate; -- unset Client-Cert request header if lighttpd did not verify client cert -- (to ignore Client-Cert request header if supplied by malicious client) r.req_header["Client-Cert"] = cert
请求限制器¶
-- lua request limiter
-- (requires lighttpd 1.4.65+)
--
-- In this example, for a given path or set of paths (determined after
-- request headers have been received), count number of total requests
-- and number of requests matching the current client remote address.
--
-- Adapt this to your site and your application needs.
local count_limit = 8 -- max 8 total requests at a time to given paths
local raddr_limit = 2 -- max 2 requests at a time per remote addr
local addr = lighty.r.req_attr["request.remote-addr"]
local ireq_attr, count, raddr = nil, 0, 0
for i in lighty.server.irequests() do
ireq_attr = i.req_attr
local path = ireq_attr["uri.path"]
-- example: interested only in "^/$" and "\.php$" paths
if (path == "/" or string.match(path, "%.php$")) then
-- save desired info; i invalid outside lighty.server.irequests iteration
count = count + 1
if (count > count_limit) then break end
if (ireq_attr["request.remote-addr"] == addr) then
raddr = raddr + 1
if (raddr > raddr_limit) then break end
end
end
end
-- check if counts exceed policy
if (count > count_limit or raddr > raddr_limit) then
-- (optional) logging
--print(addr .. ' turned away. Too many connections.')
-- 503 Service Unavailable
lighty.r.resp_header["Retry-After"] = "2"
return 503
-- 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)
-- could reject requests with 403 Forbidden
end
-- 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.
--
-- For high-traffic sites, a more efficient solution may be to implement
-- the request rate limiter in the backend daemon servicing the requests.
-- Requests can be accepted and entered into backend queue up to a limit before
-- turning away new requests. A fixed number of threads can service the queue.
其他解决方案¶
外部静态
我曾在一个地方看到过一个很好的解决方案,他们将一些文件本地托管在自己的机器上。如果流量过高、文件太大或出于任何原因,文件会被转移到亚马逊 S3 或 Akamai,以加快服务速度或应对高流量。您仍然可以使用您的主机名、URL,并从日志中收集统计数据——您的用户只需通过 302 重定向到他们请求的文件。
2008-11-17:找到了来源: presto-move-content-to-s3-with-no-code-changes
Request -> check for local copy -> 302 (if not stored locally) -> let users download from a big pipe
将以下内容添加到您的 lighttpd.conf 中
$HTTP["url"] =~ "^/static/[^/]+[.]gif([?].*)?$" { #match the files you want this to work for
magnet.attract-physical-path-to = ( "/path-to-your/external-static.lua" )
}
将以下内容保存到 external-static.lua
local filename = lighty.env["physical.path"] local stat = lighty.stat( filename ) if not stat then local static_name = string.match( filename, "static/([^/]+)$" ) lighty.header["Location"] = "http://<new-location-with-big-pipes>/" .. static_name return 302 end