lilpacks/reliw
| d | .githooks/ |
| d | build/ |
| d | cli/ |
| d | reliw/ |
| - | .gitignore |
| - | LICENSE |
| - | README.md |
| - | lilpack.json |
| - | reliw.lua |
RELIW Operations and Development Guide
Overiew
This guide documents the current RELIW behavior. It is intended for both operators (deployment and troubleshooting) and developers (schema, routing, and failure semantics).
Architecture
RELIW uses a coroutine-per-connection model built on the LEV async
I/O runtime. The manager process forks listener processes (IPv4, IPv6,
metrics), and each listener runs a lev.run() event loop that accepts
connections and spawns a coroutine per client. Redis access uses
redis (async Redis client) injected into the request handler
context. TLS with SNI is handled by LEV's LITLS integration.
Legacy config field names fork_limit and requests_per_fork are
accepted as aliases for connection_limit and requests_per_connection.
1. Quick Start
1.1 Runtime entry point
The standalone RELIW app entry point is defined in cli/reliw.lua and runs:
local reliw = require("reliw")
math.randomseed(os.time())
local reliw_srv, err = reliw.new()
if not reliw_srv then
print("failed to init RELIW: " .. tostring(err))
os.exit(-1)
end
reliw_srv:run()
1.2 Config file location
RELIW reads config JSON from:
RELIW_CONFIG_FILEenvironment variable, if set/etc/reliw/config.jsonotherwise
1.3 Minimal HTTP config
{
"ip": "127.0.0.1",
"port": 8080
}
1.4 Minimal Redis data for one host
Example below serves / for example.com from data_dir/example.com/index.md.
redis-cli -n 13 SET RLW:API:example.com '[["/","home",true]]'
redis-cli -n 13 SET RLW:API:example.com:home '{"methods":{"GET":true,"HEAD":true},"index":"index.md"}'
Create content file:
mkdir -p /www/example.com
printf '# Hello\n' > /www/example.com/index.md
Request:
curl -H 'Host: example.com' http://127.0.0.1:8080/
1.5 Manager inspection API
The reliw.new() manager object exposes read-only inspection helpers:
has_child_pid(pid)-> booleanlist_child_pids()-> map ofpid -> process_nameget_process_pids()->{ ipv4 = pid|nil, ipv6 = pid|nil, metrics = pid|nil }
These are intended for runtime introspection and tests, replacing direct reads of internal state tables.
2. Full Configuration Reference
RELIW combines defaults from reliw.lua and http.server.
2.1 Top-level server config
| Key | Default | Notes |
|---|---|---|
ip | 127.0.0.1 | IPv4 bind address |
port | 8080 | Bind port |
ipv6 | unset | If set, manager spawns IPv6 server process |
data_dir | /www | Static/dynamic content root |
cache_max_size | 5242880 | Max bytes for cached file content in Redis |
backlog | 256 | Listen backlog |
connection_limit | 64 | Max concurrent connections per listener process |
requests_per_connection | 512 | Requests handled on one connection before close |
max_body_size | 5242880 | Request body cap for content-length and chunked uploads |
request_line_limit | 8192 | Max request line or header line bytes |
keepalive_idle_timeout | 15 | Keep-alive idle timeout (seconds) |
request_header_timeout | 10 | Header read timeout (seconds) |
request_body_timeout | 30 | Body read timeout (seconds) |
tls_handshake_timeout | 10 | Server-side TLS handshake timeout (seconds) |
log_level | access | Logger level passed to std.logger |
log_headers | ["referer","x-real-ip","user-agent"] | Request headers copied into access logs |
compression | enabled | Response compression policy (gzip preferred, deflate fallback) |
redis | object | Redis connection and namespace config |
metrics | object | Metrics listener + SCAN tuning |
ssl | unset | Enable HTTPS listener (see TLS section) |
Compression defaults:
compression.enabled = truecompression.min_size = 4096compression.typesincludestext/html,text/plain,text/css,text/javascript,image/svg+xml,application/json,application/rss+xmlEncoding selection: prefers
gzipwhen client advertises it, falls back todeflateResponses include
Vary: Accept-Encodingwhen compressed
2.2 Redis config (redis)
| Key | Default | Notes |
|---|---|---|
host | 127.0.0.1 | Redis host |
port | 6379 | Redis port |
db | 13 | Selected DB |
prefix | RLW | Key namespace prefix |
timeout | 5 | Socket timeout (seconds) |
ssl | unset | Optional Redis TLS mode |
auth | unset | Optional auth object for Redis AUTH |
2.3 Metrics config (metrics)
| Key | Default | Notes |
|---|---|---|
ip | 127.0.0.1 | Metrics listener bind IP |
port | 9101 | Metrics listener bind port |
disabled | false | If true, metrics process is not spawned |
scan_count | 100 | Redis SCAN count hint; clamped to 1..1000 |
scan_limit | 2000 | Max keys inspected per scrape; clamped to 1..10000 |
Metrics process behavior:
spawned by manager as a dedicated process
uses
reliw.metrics.showforces
ssl = nilandlog_level = 100reuses manager-loaded config in memory (no second config-file read during spawn)
2.4 TLS config (ssl)
Server-side TLS config shape:
{
"ssl": {
"default": { "cert": "/path/default.crt", "key": "/path/default.key" },
"hosts": {
"*.example.org": { "cert": "/path/_.example.org.crt", "key": "/path/_.example.org.key" },
"other-site.com": { "cert": "/path/other-site.com.crt", "key": "/path/other-site.com.key" }
}
}
}
Notes:
ssl.defaultis required when TLS is enabled.ssl.hostsadds SNI contexts for additional hostnames.ssl.hostskeys support the*.example.orgwildcard syntax to match any single-label subdomain (e.g.api.example.org). Exact hostname entries take priority over wildcard entries when both match.RELIW validates that configured cert/key files exist before startup.
3. Redis Data Model
All keys are prefixed with redis.prefix (default RLW).
3.1 Routing and entry metadata
${PREFIX}:API:<host>JSON array of route entries:
[pattern, entry_id, exact_match?]exact_match(true) means strict equality; otherwisequery:match(pattern)is used
${PREFIX}:API:<host>:<entry_id>JSON object containing entry metadata
Common metadata fields:
Required:
methodsmap, for example{ "GET": true, "POST": true }
Optional:
file: explicit file pathindex: appended when query ends with/try_extensions: try.lua,.dj,.mdif file is missinggsub:{ "pattern": "...", "replacement": "..." }query remaptitle,css_file,favicon_filecache_control(for examplemax-age=3600)path_cache: iftrueandfileis set, RELIW checks${PREFIX}:FILES:<host>:<query>for a pre-computed response before loading the handler. On a cache hit the handler is skipped entirely. The handler is responsible for writing that key (fields:content,hash,size,mime,title) and callingEXPIRE. Cache invalidation is performed byDEL-ing the key.auth: see auth sectionrate_limit: see rate-limiting sectionerror: status-specific image/html override map
3.2 Content and templates
${PREFIX}:FILES:<host>:<filename>(hash)fields:
content,hash,size,mime,titlecache TTL: 3600 seconds
${PREFIX}:FILES:<host>:<query>(hash, same fields)written by Lua handlers that use
path_cache: truekey uses the request URL path (e.g.
/std/fs) rather than the handler filenameTTL is set by the handler; recommended to match
cache_control
${PREFIX}:TITLES:<host>(hash)optional per-file title override
${PREFIX}:DATA:<host>:<name>(string)user data; fallback key:
${PREFIX}:DATA:__:<name>template.luais used as page template override if presentACME HTTP-01 challenge payloads can be provisioned at
${PREFIX}:DATA:<host>:.well-known/acme-challenge/<token>
3.3 Auth/session keys
${PREFIX}:USERS:<host>(hash)field: username
value: JSON
{ "pass": "<hex_hmac>", "salt": "<salt>" }
${PREFIX}:SESSIONS:<host>:<token>(string with TTL)value: username
3.4 Proxy metadata schema
${PREFIX}:PROXY:<host>(JSON object)target(required): upstream hostscheme(optional):http(default) orhttpsport(optional): defaults to80/443by schemetls_cafile,tls_capath,tls_handshake_timeout(optional)tls_insecure,tls_no_verify,no_verify_mode(optional bools; any true enables no-verify mode)
3.5 WAF and control channels
${PREFIX}:WAF(hash)field
__: global rule set JSONfield
<host>: per-host rule set JSON
${PREFIX}:WAFFERS(Pub/Sub channel)receives blocked IP value from WAF branch
${PREFIX}:CTL(Pub/Sub channel)generic control messages from
store:send_ctl_msg
3.6 Metrics and rate-limit keys
${PREFIX}:METRICS:<host>:total(hash: status_code -> count)${PREFIX}:METRICS:<host>:by_method(hash: method -> count)${PREFIX}:METRICS:<host>:by_request(hash: query -> count; internal, 24h TTL)${PREFIX}:METRICS:<host>:timing(hash:<name>_sum/<name>_countpairs)request_sum/request_count— total handler durationproxy_sum/proxy_count— upstream proxy round-trip (proxied vhosts only)content_sum/content_count— content fetch + rendering
${PREFIX}:METRICS:<host>:waf_blocks(hash: rule_source -> count)"global"— blocks from the global WAF rule set (__)"<hostname>"— blocks from a per-host WAF rule set
${PREFIX}:METRICS:misdirected(string counter — total 421 responses for non-configured vhosts)${PREFIX}:LIMITS:<host>:<method>:<query>:<ip>(string counter with TTL)
4. WAF Behavior
Rule document format (global or per-host):
{
"ip_header": "x-forwarded-for",
"query": ["^/admin", "drop%stable"],
"headers": {
"user-agent": ["badbot", "sqlmap"],
"x-custom": ["evil"]
}
}
Semantics:
Matching uses Lua pattern matching (
string.match), not PCRE.Evaluation order:
global query rules
global header rules
per-host query rules
per-host header rules
Default blocked-IP header source is
x-forwarded-forifip_headeris missing.On match:
publishes IP to
${PREFIX}:WAFFERSlogs blocked event with rule and host
increments
http_waf_blocks_totalcounter withrule="global"orrule="<host>"returns
301tohttp://127.0.0.1/Fuck_Off
5. Request Handling Semantics
Main handler: reliw/handle.lua.
Order of operations:
Initialize Redis-backed store for request.
Normalize/validate host and query.
Verify host is a configured vhost (
421if not).Evaluate WAF.
Check host-level proxy config.
Resolve route metadata.
Apply auth, method checks, and rate limits.
Load/render content.
Apply ETag/cache semantics and update metrics.
Client IP normalization at request ingress:
x-client-ipis always set from socket peer IP.x-real-ipis only auto-filled from peer IP when request does not provide it.Downstream RELIW logic prefers
x-client-ip.
5.1 Host and query hardening
Host validation:
accepts DNS name,
localhost, IPv4, bracketed IPv6rejects malformed ports, comma-separated host lists, control chars, unbracketed IPv6
Query validation:
requires leading
/rejects control chars and backslashes
rejects encoded traversal separators (
%2e,%2f,%5c)percent-decodes and rejects
..segments
5.2 Auth flow
metadata.auth supports three paths:
login endpoint mode (
metadata.auth.login == true)GET: returns login formPOST: parses body form fieldsloginandpasswordsuccessful auth: sets
rlw_session_token=<token>; secure; HttpOnlyand303redirectfailed/malformed auth body: deterministic
401
logout endpoint mode (
metadata.auth.logout == true)clears
rlw_session_tokenandrlw_redirect, returns303to/
allowlist mode (
metadata.authis a username list)unauthenticated request:
302to/loginand setsrlw_redirect=<query>
5.3 Proxy routing behavior
If ${PREFIX}:PROXY:<host> exists, RELIW proxies request and skips local content flow.
Current proxy behavior:
upstream connect over TCP; TLS wrap+handshake when
scheme == "https"rewrites:
Host-> upstream hostOrigin/Referer-> upstream originadds
X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For
response handling:
supports chunked responses with chunk extensions
normalizes content length
rewrites CORS allow-origin to original origin/host
ensures proxied
Set-CookieincludesSecure
5.4 ETag and method semantics
For content responses, ETag is generated from SHA-256 of content.
Conditional behavior:
GET+ matchingIf-None-Match->304with empty bodyHEAD->200with empty body, includes ETag/content-lengthnon-
GET/HEADdoes not use ETag short-circuit
5.5 Content rendering
Markdown and Djot (text/markdown, text/djot):
Content is rendered to HTML by the bundled
markdownmodule.If the rendered HTML begins with an
<h1>element, its text is automatically extracted and used as the page title (overriding anymetadata.titleorRLW:TITLESvalue). The<h1>is then stripped from the body so it is not duplicated alongside the<header>section injected by the page template.If no
<h1>is present, the title falls back tometadata.title, then to theRLW:TITLES:<host>Redis hash, then to empty string.When the request
Acceptheader includestext/markdown, the raw source is returned as-is with the original MIME type (no rendering or title extraction).
Lua handlers (application/lua):
The
.luafile is loaded once, cached as bytecode inRLW:FILES:<host>:<filename>, and executed on every request ashandler(method, query, args, headers, body).The handler may return
(content, status)— RELIW wrapscontentwith the page template — or(content, status, headers_table)— RELIW usesheaders_tabledirectly and skips template wrapping.When
path_cache: trueis set in entry metadata, RELIW checksRLW:FILES:<host>:<query>before invoking the handler. If a cached response is present it is served immediately without handler execution.
6. Failure-Mode Responses
Common status outcomes:
| Status | Trigger |
|---|---|
400 | Invalid host header or invalid query |
421 | Request to non-configured vhost |
401 | Login failure (wrong creds or malformed body) |
404 | Route/content missing; non-/metrics on metrics listener |
405 | Method not allowed by entry metadata |
429 | Rate limit exceeded |
500 | Metadata/content/Lua content execution failures |
502 | Upstream proxy failures |
503 | Store initialization failure (main handler or metrics handler) |
Additional behavior:
WAF block returns
301with redirect to local sink URL.Unauthorized protected content returns
302to/login.Logout returns
303to/.
7. Observability
7.1 Logs
Main listener and metrics process both emit structured logs via std.logger.
Access-style request logs include:
vhost,method,query,status,process,size,timeclient_ip(always present; socket peer address)plus configured
log_headersif present in requestoptional forwarded context when present:
forwarded_for(from requestx-forwarded-for)forwarded_real_ip(from requestx-real-ipwhen different fromclient_ip)
Important explicit log events:
store init failedinvalid host headerinvalid queryblocked by WAFproxy startup/errors and metrics store init failures
7.2 Metrics endpoint
Metrics listener:
GET /metrics-> Prometheus text formatany other path ->
404
Exported families:
http_requests_total{host="<host>",code="<status>"} <count>http_requests_by_method{host="<host>",method="<method>"} <count>http_waf_blocks_total{host="<host>",rule="<source>"} <count>ruleis"global"for global WAF rules, or the domain name for per-host rules
http_misdirected_total <count>(requests to non-configured vhosts)http_request_duration_seconds_sum{host="<host>"} <seconds>(summary)http_request_duration_seconds_count{host="<host>"} <count>http_proxy_duration_seconds_sum{host="<host>"} <seconds>(summary, proxied vhosts only)http_proxy_duration_seconds_count{host="<host>"} <count>http_content_duration_seconds_sum{host="<host>"} <seconds>(summary)http_content_duration_seconds_count{host="<host>"} <count>
All metrics commands per request are batched into a single Redis pipeline.
8. Testing
RELIW regression tests are not yet implemented.