Files
proxy/nodejs/services/host_lookup.js
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

87 lines
3.4 KiB
JavaScript

'use strict';
const {Host} = require('../models/host');
const {SocketServerJson} = require('../utils/unix_socket_json');
const conf = require('@simpleworkjs/conf');
/**
* Host Lookup Service
*
* Unix socket server that handles host/domain lookup requests from OpenResty.
* This provides the bridge between nginx (Lua) and the Node.js host management system.
*
* Flow:
* 1. OpenResty sends domain lookup request via Unix socket
* 2. Service queries Host model (supports wildcards via lookup tree)
* 3. Returns host configuration (IP, port, SSL settings, etc.)
* 4. All values converted to strings for Redis compatibility
*
* Redis Compatibility:
* All object values are converted to strings before sending because:
* - Redis stores everything as strings
* - OpenResty's primary lookup path uses Redis directly (hgetall)
* - This socket is a fallback when Redis cache misses
* - Both paths must return identical data structures to Lua consumer
*/
const socket = new SocketServerJson({
socketFile: conf.socketFile,
onData: function(data, clientSocket) {
try{
// Try to match the requested host name using the lookup tree
let parentHost = Host.lookUp(data['domain']);
// If we don't have a match, return empty object
if(!parentHost) return clientSocket.write(JSON.stringify({}) + '\n');
// lookUp returns the live #record object stored inside the shared
// lookup tree. Everything below mutates parentHost (sets
// wildcard_parent, stringifies every value), so work on a shallow
// copy — mutating the shared node would corrupt the tree, e.g. turn
// wildcard_matchAny into the string "false" (truthy) and break the
// matchAny guard on subsequent lookups.
parentHost = {...parentHost};
// A wildcard host with matchAny disabled only serves subdomains that
// are explicitly defined in redis. Reaching this service means redis
// had no direct entry for the requested domain, so an inexact match
// here (the request isn't the wildcard host itself) is an undefined
// subdomain and must not be routed to the wildcard parent.
if(parentHost.is_wildcard && !parentHost.wildcard_matchAny
&& parentHost.host !== data['domain']){
return clientSocket.write(JSON.stringify({}) + '\n');
}
// If the matched host belongs to a wildcard domain, set wildcard_parent
// This allows child domains to use the parent's wildcard SSL certificate
if(!parentHost.wildcard_parent){
parentHost.wildcard_parent = parentHost.host;
Host.addCache(data['domain'], parentHost);
}
// Convert all values to strings for Redis compatibility
// OpenResty expects the same data format from both Redis and this socket
for(const [key, value] of Object.entries(parentHost)) {
parentHost[key] = String(value);
}
// Terminate with a newline: the Lua client (ops/nginx_conf/targetinfo.lua)
// reads a single line per lookup via a cosocket receive() -- without a
// delimiter it would block for the full read timeout on every request
// waiting for a newline that never arrives (this was masked before by
// blocking LuaSocket's timeout+partial-read behavior, which silently
// paid that same timeout on every single lookup).
clientSocket.write(JSON.stringify(parentHost) + '\n');
}catch(error){
console.error('services/host_lookup onData error', error);
}
},
onListen: function(){
console.log('Host lookup service listening on', conf.socketFile);
}
});
module.exports = {socket};