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} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ 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