项目

常规

个人资料

操作

AbsoLUAtion - Lighttpd + Lua 的强大组合

我们希望为 lighttpd + Lua 构建一个中心资源,因为这是 lighttpd 相对于其他 Web 服务器的最大优势之一。它有用、方便、简单,有时是一个非常强大的组合,能为您提供额外的灵活性,并提供其他 HTTP 服务器无法解决的大小问题的解决方案!

再次,我们希望您,lighttpd 的用户,通过贡献链接、代码片段,或者仅仅是提供您的 Lua 脚本并附带它们功能的小描述以及它们如何帮助 lighttpd 完成您想要其完成的任务来支持本页面。

要求

链接

死链接?您不希望在此处列出?请将其删除。谢谢!

代码片段

Apache .htaccess 替代方案

将 .htaccess 功能迁移到 lighttpd 的选项

Apache .htaccess 中的常见用法
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

lighttpd 替代方案
  • 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 绑定。使用修补的 libmodsecuritycffi-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 

示例文件

更新者 gstrauss 2 年多前 · 55 次修订