master
raw

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:

  1. RELIW_CONFIG_FILE environment variable, if set

  2. /etc/reliw/config.json otherwise

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) -> boolean

  • list_child_pids() -> map of pid -> process_name

  • get_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

KeyDefaultNotes
ip127.0.0.1IPv4 bind address
port8080Bind port
ipv6unsetIf set, manager spawns IPv6 server process
data_dir/wwwStatic/dynamic content root
cache_max_size5242880Max bytes for cached file content in Redis
backlog256Listen backlog
connection_limit64Max concurrent connections per listener process
requests_per_connection512Requests handled on one connection before close
max_body_size5242880Request body cap for content-length and chunked uploads
request_line_limit8192Max request line or header line bytes
keepalive_idle_timeout15Keep-alive idle timeout (seconds)
request_header_timeout10Header read timeout (seconds)
request_body_timeout30Body read timeout (seconds)
tls_handshake_timeout10Server-side TLS handshake timeout (seconds)
log_levelaccessLogger level passed to std.logger
log_headers["referer","x-real-ip","user-agent"]Request headers copied into access logs
compressionenabledResponse compression policy (gzip preferred, deflate fallback)
redisobjectRedis connection and namespace config
metricsobjectMetrics listener + SCAN tuning
sslunsetEnable HTTPS listener (see TLS section)

Compression defaults:

  • compression.enabled = true

  • compression.min_size = 4096

  • compression.types includes text/html, text/plain, text/css, text/javascript, image/svg+xml, application/json, application/rss+xml

  • Encoding selection: prefers gzip when client advertises it, falls back to deflate

  • Responses include Vary: Accept-Encoding when compressed

2.2 Redis config (redis)

KeyDefaultNotes
host127.0.0.1Redis host
port6379Redis port
db13Selected DB
prefixRLWKey namespace prefix
timeout5Socket timeout (seconds)
sslunsetOptional Redis TLS mode
authunsetOptional auth object for Redis AUTH

2.3 Metrics config (metrics)

KeyDefaultNotes
ip127.0.0.1Metrics listener bind IP
port9101Metrics listener bind port
disabledfalseIf true, metrics process is not spawned
scan_count100Redis SCAN count hint; clamped to 1..1000
scan_limit2000Max keys inspected per scrape; clamped to 1..10000

Metrics process behavior:

  • spawned by manager as a dedicated process

  • uses reliw.metrics.show

  • forces ssl = nil and log_level = 100

  • reuses 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.default is required when TLS is enabled.

  • ssl.hosts adds SNI contexts for additional hostnames.

  • ssl.hosts keys support the *.example.org wildcard 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; otherwise query:match(pattern) is used

  • ${PREFIX}:API:<host>:<entry_id>

    • JSON object containing entry metadata

Common metadata fields:

  • Required:

    • methods map, for example { "GET": true, "POST": true }

  • Optional:

    • file: explicit file path

    • index: appended when query ends with /

    • try_extensions: try .lua, .dj, .md if file is missing

    • gsub: { "pattern": "...", "replacement": "..." } query remap

    • title, css_file, favicon_file

    • cache_control (for example max-age=3600)

    • path_cache: if true and file is 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 calling EXPIRE. Cache invalidation is performed by DEL-ing the key.

    • auth: see auth section

    • rate_limit: see rate-limiting section

    • error: status-specific image/html override map

3.2 Content and templates

  • ${PREFIX}:FILES:<host>:<filename> (hash)

    • fields: content, hash, size, mime, title

    • cache TTL: 3600 seconds

  • ${PREFIX}:FILES:<host>:<query> (hash, same fields)

    • written by Lua handlers that use path_cache: true

    • key uses the request URL path (e.g. /std/fs) rather than the handler filename

    • TTL 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.lua is used as page template override if present

    • ACME 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 host

    • scheme (optional): http (default) or https

    • port (optional): defaults to 80/443 by scheme

    • tls_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 JSON

    • field <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>_count pairs)

    • request_sum / request_count — total handler duration

    • proxy_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:

5. Request Handling Semantics

Main handler: reliw/handle.lua.

Order of operations:

  1. Initialize Redis-backed store for request.

  2. Normalize/validate host and query.

  3. Verify host is a configured vhost (421 if not).

  4. Evaluate WAF.

  5. Check host-level proxy config.

  6. Resolve route metadata.

  7. Apply auth, method checks, and rate limits.

  8. Load/render content.

  9. Apply ETag/cache semantics and update metrics.

Client IP normalization at request ingress:

5.1 Host and query hardening

Host validation:

  • accepts DNS name, localhost, IPv4, bracketed IPv6

  • rejects 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 form

    • POST: parses body form fields login and password

    • successful auth: sets rlw_session_token=<token>; secure; HttpOnly and 303 redirect

    • failed/malformed auth body: deterministic 401

  • logout endpoint mode (metadata.auth.logout == true)

    • clears rlw_session_token and rlw_redirect, returns 303 to /

  • allowlist mode (metadata.auth is a username list)

    • unauthenticated request: 302 to /login and sets rlw_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 host

    • Origin/Referer -> upstream origin

    • adds 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-Cookie includes Secure

5.4 ETag and method semantics

  • For content responses, ETag is generated from SHA-256 of content.

  • Conditional behavior:

    • GET + matching If-None-Match -> 304 with empty body

    • HEAD -> 200 with empty body, includes ETag/content-length

    • non-GET/HEAD does not use ETag short-circuit

5.5 Content rendering

Markdown and Djot (text/markdown, text/djot):

  • Content is rendered to HTML by the bundled markdown module.

  • If the rendered HTML begins with an <h1> element, its text is automatically extracted and used as the page title (overriding any metadata.title or RLW:TITLES value). 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 to metadata.title, then to the RLW:TITLES:<host> Redis hash, then to empty string.

  • When the request Accept header includes text/markdown, the raw source is returned as-is with the original MIME type (no rendering or title extraction).

Lua handlers (application/lua):

  • The .lua file is loaded once, cached as bytecode in RLW:FILES:<host>:<filename>, and executed on every request as handler(method, query, args, headers, body).

  • The handler may return (content, status) — RELIW wraps content with the page template — or (content, status, headers_table) — RELIW uses headers_table directly and skips template wrapping.

  • When path_cache: true is set in entry metadata, RELIW checks RLW: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:

StatusTrigger
400Invalid host header or invalid query
421Request to non-configured vhost
401Login failure (wrong creds or malformed body)
404Route/content missing; non-/metrics on metrics listener
405Method not allowed by entry metadata
429Rate limit exceeded
500Metadata/content/Lua content execution failures
502Upstream proxy failures
503Store initialization failure (main handler or metrics handler)

Additional behavior:

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, time

  • client_ip (always present; socket peer address)

  • plus configured log_headers if present in request

  • optional forwarded context when present:

    • forwarded_for (from request x-forwarded-for)

    • forwarded_real_ip (from request x-real-ip when different from client_ip)

Important explicit log events:

  • store init failed

  • invalid host header

  • invalid query

  • blocked by WAF

  • proxy startup/errors and metrics store init failures

7.2 Metrics endpoint

Metrics listener:

  • GET /metrics -> Prometheus text format

  • any 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>

    • rule is "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.