Files
proxy/ops/nginx_conf/targetinfo.lua
T
wmantly a19ff81c76 Fix a worker-blocking Lua socket call and add gzip/caching for static assets
- ops/nginx_conf/targetinfo.lua's wildcard-subdomain lookup fallback used
  classic LuaSocket (require("socket.unix")) instead of an OpenResty
  cosocket. LuaSocket is blocking, and called from an nginx worker it
  stalls the ENTIRE worker — every other in-flight connection on it — for
  the round-trip to the Node app. Worse, the Node side never
  newline-terminated its response, so the old blocking receive() only ever
  returned via its read-timeout-then-partial-read fallback, meaning every
  single cache-miss lookup paid a fixed timeout penalty while blocking the
  whole worker. Replaced with an ngx.socket.tcp() cosocket (unix-domain via
  "unix:/path", the only cosocket API this lua-nginx-module ships) and
  newline-terminated the Node service's responses so receive() actually
  completes instead of timing out. Verified against a live container:
  previously this crashed OpenResty's Lua VM entirely
  (ngx.socket.unix doesn't exist); fixed version resolves fresh wildcard
  subdomains in ~2ms.
- Add gzip compression (`compression` middleware) and far-future
  Cache-Control on static assets (7d for vendor libs under
  /static-modules, 1h for the app's own /static JS/CSS, which isn't
  cache-busted). The admin UI is a traditional multi-page app that loads
  ~13 separate vendor/app JS+CSS files on every full navigation; previously
  none of them were compressed and Cache-Control was `max-age=0` (Express's
  default), forcing a revalidation round-trip for every asset on every page
  view.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 00:41:31 -04:00

109 lines
4.2 KiB
Lua

local M = {}
-- Query the Node app's host-lookup service for a domain that missed the
-- Redis fast path (wildcard subdomains not yet cached, see Host.addCache).
-- Uses an OpenResty cosocket rather than the classic LuaSocket socket.unix()
-- the previous version of this file used: LuaSocket's API is blocking and,
-- called from an nginx worker, stalls the ENTIRE worker (every other
-- in-flight connection on it) for the round-trip -- a real source of
-- intermittent request latency for any wildcard host whose on-demand cache
-- entry (1h TTL, conf.cacheTTL) had expired. resty.redis (used just above)
-- is cosocket-based already and works fine from both the phases this module
-- is called from (access_by_lua_block and the SSL request_domain callback),
-- so a unix-domain cosocket is safe here too.
local function unixLookup(json, domain)
-- The ngx_lua cosocket API has no separate ngx.socket.unix -- a plain
-- ngx.socket.tcp() connects to a unix domain socket when given a
-- "unix:/path" address instead of a host/port pair.
local sock = ngx.socket.tcp()
sock:settimeouts(100, 100, 100) -- connect, send, read (ms)
local ok = sock:connect("unix:/var/run/proxy_lookup.socket")
if not ok then return nil end
local ok = sock:send(json.encode({domain = domain}))
if not ok then
sock:close()
return nil
end
local line = sock:receive()
sock:close()
if not line then return nil end
local decodeOk, decoded = pcall(json.decode, line)
if not decodeOk then return nil end
return decoded
end
print("In targetInfo module")
-- Main function of the module. Returns (res) on success, or (nil, httpStatus)
-- on failure -- it must NOT call ngx.exit() itself: this is called both from
-- proxy.conf's access_by_lua_block (a normal request phase, where ngx.exit()
-- is fine) AND from nginx.conf's request_domain callback, which runs during
-- the TLS handshake (ssl_certificate_by_lua*). ngx.exit() is not a supported
-- API in that phase -- calling it there aborts the handshake with a bare
-- "internal error" TLS alert and no log output, breaking TLS entirely
-- (including the self-signed fallback cert, since auto-ssl never gets to
-- fall back gracefully). Callers that can legitimately abort the request
-- (i.e. proxy.conf) must call ngx.exit() themselves using the returned status.
function M.get(ngx, domain, targetInfo)
-- Reuse a previously-resolved target ONLY when it was resolved for this
-- exact host. HTTP/2 connection coalescing lets a browser serve several
-- hostnames that share one wildcard cert (e.g. *.718it.biz) over a single
-- connection; the SSL phase (request_domain) resolves and caches the
-- connection's first host in ngx.ctx.targetInfo. Without the domain check
-- below, every coalesced request on that connection would be handed the
-- first host's target -- e.g. hassio.718it.biz served from metrics.718it.biz.
if targetInfo and ngx.ctx.targetInfo_domain == domain then
return targetInfo
end
local json = require "cjson"
local redis = require "resty.redis"
if not domain then
return nil, 499
end
local red = redis:new()
red:set_timeout(1000) -- 1 second
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect to redis: ", err)
return nil, 598
end
local res, err = red:hgetall("proxy_Host_"..domain)
res = red:array_to_hash(res)
-- Return the connection to the pool instead of closing it, so it can be
-- reused by later requests. Without this a new connection is opened per
-- request and never released, exhausting sockets under load.
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.log(ngx.ERR, "failed to set redis keepalive: ", err)
end
if not res["ip"] then
res = unixLookup(json, domain) or res
end
if not res["ip"] then
return nil, 406
end
ngx.ctx.targetInfo = res
-- Remember which host this target was resolved for, so the reuse guard at
-- the top can tell a genuine cache hit from a coalesced request for a
-- different host on the same connection.
ngx.ctx.targetInfo_domain = domain
ngx.ctx.toAllow = true
return res
end
return M