gizlang:gateway
the front door of a host. One program takes the HTTP and HTTPS ports (HTTP/1.1, HTTP/2 and HTTP/3), picks a route for each request by its host and path, and relays the request to the route's upstreams (Http.Balancer: turns, failover, streamed bodies, WebSocket and other upgrades). A JSON file describes the routes and the certificates; the program reads it again when the file changes, and reloads a certificate when its file changes, with no restart and no dropped connection. It counts every request in gizlang:metrics (GET /metrics on its own port) and writes one JSON line for each to an access log.
{ "http": 80, "https": 443, "host": "", "metrics": "127.0.0.1:18082", "accessLog": "/var/log/gateway/access.log", "challenges": "/var/lib/gateway/challenges", "headers": { "strict-transport-security": "max-age=31536000; includeSubDomains" }, "certs": [ { "names": ["example.com", ".example.com"], "chain": "/etc/gateway/a.crt", "key": "/etc/gateway/a.key" }, { "names": ["api.example.com"], "acme": { "folder": "/var/lib/gateway/api", "contact": "mailto:ops@example.com" } } ], "routes": [ { "name": "site", "hosts": ["example.com", ".example.com"], "prefix": "/", "stripPrefix": false, "upstreams": ["http://10.0.0.2:8800"], "standby": ["http://10.0.0.3:8800"], "holdMs": 2000, "keepHost": true, "headers": {}, "upstreamHeaders": {}, "priority": 0, "responseMs": 120000, "pathRegex": "^/v[0-9]+/", "hostRegex": "^app[0-9]+\.example\.com$", "basicAuth": { "realm": "private", "users": ["alice:$apr1$salt$hash", "bob:{SHA}base64sha1"] }, "retry": { "attempts": 2, "statuses": [502] }, "errors": { "statuses": [502, 503, 504], "upstream": "http://127.0.0.1:18089" } }, { "name": "old", "hosts": ["old.example.com"], "redirect": { "to": "https://example.com/", "status": 301 } } ] }
A host is exact or *.suffix; no hosts means any. A prefix matches a whole path segment ("/api" matches "/api" and "/api/x"). Of the routes that match, an exact host beats a wildcard, a wildcard beats none, and then the longer prefix wins. The first certificate answers a client that names no host or one no certificate has; the others answer for their names (the server name of the handshake). pathRegex and hostRegex must also match the request path (without its query) and the host (lower case, without a port). basicAuth answers 401 unless the request has a listed user and password; a user line is an htpasswd line with an $apr1$, $1$, $2y$ (bcrypt) or {SHA} hash (a bcrypt hash $2y$ is checked once per password and remembered). A higher priority beats every other rule. standby upstreams get a request only when no upstream can take it (a refused connection passes on any request, since nothing was sent). responseMs is the longest wait for an upstream's response head. upstreamHeaders are set on each request the route sends on, in place of any copy the client sent (a key the upstream checks). retry sends an idempotent request again, up to attempts more times, when an upstream answers with one of the statuses. errors answers with the page that upstream serves at "/", under the upstream's own status, when that status is listed or no upstream can be reached (502). A route with redirect has no upstreams: it answers with the status (302 unless given) and the Location to. A top-level limit { "requests": 600, "windowMs": 60000, "proxies": ["10.0.0.1"] } answers 429 with Retry-After to a client over it, before any route. A request from one of the proxies keeps the scheme of its X-Forwarded-Proto (a TLS proxy in front of a plain port).
import * as Gateway from "gizlang:gateway";Types
RouteConfig
type RouteConfig = { name: string; hosts: Vec<string>; prefix: string; strip: bool; upstreams: Vec<Http.Origin>; standby: Vec<Http.Origin>; holdMs: i64; keepHost: bool; headers: Vec<Http.Header>; upstreamHeaders: Vec<Http.Header>; priority: i64; policy: Http.Policy; redirect: string; redirectStatus: i64; pathRegex: Option<Regex>; hostRegex: Option<Regex>; realm: string; users: Vec<Login>; }One route as the file describes it.
name:stringhosts:Vec<string>prefix:stringstrip:boolupstreams:Vec<gizlang_http.Origin>standby:Vec<gizlang_http.Origin>holdMs:i64keepHost:boolheaders:Vec<gizlang_http.Header>upstreamHeaders:Vec<gizlang_http.Header>priority:i64policy:gizlang_http.Policyredirect:stringredirectStatus:i64pathRegex:_gizlang_sdk_core.Option<gizlang_regex.Regex>hostRegex:_gizlang_sdk_core.Option<gizlang_regex.Regex>realm:stringusers:Vec<gizlang_gateway.Login>
Login
type Login = { name: string; hash: string; }A user of a route and the htpasswd hash of the password.
name:stringhash:string
CertConfig
type CertConfig = { names: Vec<string>; chain: string; key: string; folder: string; directory: string; contact: string; }One certificate: the names it answers for and the PEM files of its chain and key. With folder it is issued and renewed by ACME
(HTTP-01 on the plain port) from directory, contact being "mailto:..." or "", and the files are chain.pem and domain.key in the folder.
names:Vec<string>chain:stringkey:stringfolder:stringdirectory:stringcontact:string
Limit
type Limit = { requests: i64; windowMs: i64; proxies: Vec<string>; }At most requests requests from one client (Http.clientOf: the peer, or the X-Forwarded-For hop that one of proxies appended) in
windowMs; 0 requests: no limit.
requests:i64windowMs:i64proxies:Vec<string>
Config
type Config = { http: i64; https: i64; host: string; metrics: string; accessLog: string; challenges: string; headers: Vec<Http.Header>; certs: Vec<CertConfig>; routes: Vec<RouteConfig>; threads: i64; limit: Limit; }What the file describes. http and https are ports (0: not served), metrics an address "host:port" ("": none).
http:i64https:i64host:stringmetrics:stringaccessLog:stringchallenges:stringheaders:Vec<gizlang_http.Header>certs:Vec<gizlang_gateway.CertConfig>routes:Vec<gizlang_gateway.RouteConfig>threads:i64limit:gizlang_gateway.Limit
Route
type Route = { name: string; hosts: Vec<string>; prefix: string; strip: bool; balancer: Http.Balancer; extra: Vec<Http.Header>; upstreamHeaders: Vec<Http.Header>; priority: i64; redirect: string; redirectStatus: i64; pathRegex: Option<Regex>; hostRegex: Option<Regex>; realm: string; users: Vec<Login>; verified: Ref<Vec<string>>; classes: Vec<Metrics.Series>; latency: Metrics.Series; }A route in use: its rule, its balancer and the series that count its requests.
name:stringhosts:Vec<string>prefix:stringstrip:boolbalancer:gizlang_http.Balancerextra:Vec<gizlang_http.Header>upstreamHeaders:Vec<gizlang_http.Header>priority:i64redirect:stringredirectStatus:i64pathRegex:_gizlang_sdk_core.Option<gizlang_regex.Regex>hostRegex:_gizlang_sdk_core.Option<gizlang_regex.Regex>realm:stringusers:Vec<gizlang_gateway.Login>verified:Ref<Vec<string>>classes:Vec<gizlang_metrics.Series>latency:gizlang_metrics.Series
Table
type Table = { routes: Vec<Route>; unrouted: Metrics.Series; limited: Metrics.Series; limit: Limit; metrics: Vec<Metrics.Metric>; }The routes of one configuration and the metrics that count them.
routes:Vec<gizlang_gateway.Route>unrouted:gizlang_metrics.Serieslimited:gizlang_metrics.Serieslimit:gizlang_gateway.Limitmetrics:Vec<gizlang_metrics.Metric>
Gateway
type GatewayThe running gateway: its table (replaced on a reload), its access log and its client limiter, which a reload keeps while the limit stays the same. Each serving thread has its own (a client that spreads its connections over N threads can get up to N times the limit).
Opaque: create and use it through this module's functions.
Functions
parse
function parse(source: string): Config throws stringThe configuration in TEXT, or why it cannot be used.
Effects: fail.
table
function table(pool: Http.Pool, config: Config): Table throws stringThe table of CONFIG; its upstreams share POOL. Series are global by name, so the counts go on across a reload.
Effects: fail, write, io.
gateway
function gateway(sys: System, table: Table): GatewayA gateway that routes by TABLE, with no access log.
Effects: fail, write.
run
function run(sys: System, path: string): i64Run the gateway of the file PATH until the process ends: the ports of the file, its certificates, its routes, a reload when it changes. The status for the process.
Effects: fail, read, write, console, io.
Methods
Table.pick
function Table.pick(self: Table, host: string, path: string): Option<Route>The route for a request to HOST (lower case, without a port) and PATH.
Effects: fail.
Gateway.use
function Gateway.use(self: Gateway, table: Table): voidThe routes of a new configuration take over; requests in flight finish on the old ones.
Effects: fail, read, write.
Gateway.metrics
function Gateway.metrics(self: Gateway): Vec<Metrics.Metric>The metrics of the gateway's current table.
Effects: fail, read.
Gateway.logTo
function Gateway.logTo(self: Gateway, dir: Dir, path: string): bool throws IoErrorWrite the access log to the file PATH, appending ("" closes it).
Effects: fail, read, write, io.
Gateway.answer
function Gateway.answer(self: Gateway, scheme: string): (request: Http.Request) => Http.ReplyPlanThe reply for requests that arrive by SCHEME ("http" or "https"): each is routed, relayed, counted and logged.
Gateway.uploads
function Gateway.uploads(self: Gateway, scheme: string): Http.RequestHeadChooses the requests whose body streams to the upstream: a chunked body or one of STREAM_FROM bytes or more, on a route that proxies (an upgrade, a redirect, a refusal or no route is answered as before, and the unread body ends the connection).
Constants
WATCH_MS
const WATCH_MS = 2000How often the program looks at its files, in milliseconds.
LETS_ENCRYPT
const LETS_ENCRYPT = "https://acme-v02.api.letsencrypt.org/directory"The ACME directory of a certificate that names none.
REOPEN_MS
const REOPEN_MS = 30000How often the access log file is opened again (so that a rotation takes effect), in milliseconds.