nx_pages_vhost.nx source
↩ module page · 239 lines · 9079 B
1// nx_pages_vhost.nx -- multi-virtual-host routing primitive. D1.1 of
2// the NISHI_DEPLOY_ROADMAP -- the first composition piece that lets one
3// nx_pages_daemon serve multiple domains from one ELF. Replaces the
4// equivalent of nginx's `server { server_name X; root Y; }` blocks.
5//
6// Design contract:
7// - Caller pre-allocates an NxVHostTable with N vhosts.
8// - Each NxVHost binds a domain name to a filesystem doc root.
9// - nx_vhost_dispatch(req_buf, req, table, ...) reads the parsed
10// request's Host header and returns the matched vhost's doc-root
11// range. Case-insensitive match per RFC 7230 ยง5.4.
12// - Wildcard fallback supported via a designated catch-all vhost
13// (used for the default-server-block equivalent).
14//
15// Per [[NISHI_DEPLOY_ROADMAP]] D1.1. Composes nx_http.nx http_get_header.
16//
17// nx_safety_envelope:
18// intended_use: "Multi-vhost HTTP routing for nx_pages_daemon"
19// sil_target: SIL1
20// asil_target: QM
21// evidence: [case_insensitive_per_RFC_7230_section_5.4,
22// bounded_table_no_recursion,
23// sealed_enum_verdict]
24// hazard_register: [bug-vhost-injection-via-Host-header-CR-LF,
25// bug-default-vhost-shadows-explicit-match]
26// verdict: NOT_YET_EVALUATED
27
28import "nx_syscalls.nx"
29import "nx_tier.nx"
30import "nx_http.nx"
31
32// ===== Sealed-enum: VHostDispatchResult ===========================
33const NX_VHOST_OK: nx_int = 0 // matched explicit vhost
34const NX_VHOST_FALLBACK: nx_int = 1 // matched catch-all
35const NX_VHOST_NO_MATCH: nx_int = 2 // no vhost AND no fallback
36const NX_VHOST_NO_HOST_HDR: nx_int = 3 // request had no Host: header
37const NX_VHOST_BAD_ARG: nx_int = 4 // null table / negative count
38
39// Per-vhost entry: ties a host name (e.g. "nishifamily.com") to a
40// filesystem doc root (e.g. "/share/nishi-pages/dist"). Both stored
41// as offsets into a caller-owned byte arena so the table is flat
42// and pointer-free. Stored as i64-array with stride 4 (host_name_off,
43// host_name_len, dist_root_off, dist_root_len) per nx_dashboard
44// convention -- avoids NishiLang struct-pointer-cast alignment quirks.
45const NX_VHOST_STRIDE: i64 = 4
46const NX_VHOST_HNAME_OFF: i64 = 0
47const NX_VHOST_HNAME_LEN: i64 = 1
48const NX_VHOST_ROOT_OFF: i64 = 2
49const NX_VHOST_ROOT_LEN: i64 = 3
50
51// Table of vhost entries. `fallback_idx` = -1 if no catch-all, else
52// the index of the vhost to use when no explicit match. The arena
53// holds the actual bytes referenced by host_name_off / dist_root_off.
54struct NxVHostTable {
55 arena: *u8, // byte storage for names + roots
56 arena_len: i64,
57 vhosts: *i64, // flat i64 array, n_vhosts * NX_VHOST_STRIDE entries
58 n_vhosts: i64,
59 fallback_idx: i64 // -1 or [0, n_vhosts)
60}
61
62// ===== Helpers ====================================================
63
64// ASCII lowercase a single byte (untouched if not A-Z).
65func _vh_lc(b: i64) -> i64 {
66 if b >= 0x41 { if b <= 0x5A { return b + 32 } }
67 return b
68}
69
70// Case-insensitive byte-range vs byte-range equality. Returns 1
71// if equal, 0 otherwise.
72func _vh_eq_ci(a: *u8, a_off: i64, a_len: i64,
73 b: *u8, b_off: i64, b_len: i64) -> i64 {
74 if a_len != b_len { return 0 }
75 var i: i64 = 0
76 while i < a_len {
77 let ca: i64 = _vh_lc(a[a_off + i] as i64)
78 let cb: i64 = _vh_lc(b[b_off + i] as i64)
79 if ca != cb { return 0 }
80 i = i + 1
81 }
82 return 1
83}
84
85// Strip a port suffix (`:PORT`) from a host value range. `host_header`
86// might be `nishifamily.com:443` or `nishifamily.com`; both must match
87// the same vhost entry. Returns the length of the host portion
88// (excluding the colon and port digits).
89func _vh_strip_port(buf: *u8, off: i64, len: i64) -> i64 {
90 var i: i64 = 0
91 while i < len {
92 if buf[off + i] == 0x3A { return i } // ':'
93 i = i + 1
94 }
95 return len
96}
97
98// ===== Public: build a table programmatically =====================
99//
100// Caller-allocates path:
101// let table: *NxVHostTable = sys_mmap(...) as *NxVHostTable
102// let arena: *u8 = sys_mmap(ARENA_BYTES)
103// let vhosts: *NxVHost = sys_mmap(n * sizeof(NxVHost)) as *NxVHost
104// table.arena = arena
105// table.vhosts = vhosts
106// table.n_vhosts = 0
107// table.fallback_idx = -1
108// nx_vhost_add(table, "nishifamily.com", "/share/nishi-pages/dist")
109// ... etc.
110
111func nx_vhost_table_init(t: *NxVHostTable, arena: *u8, arena_len: i64,
112 vhosts: *i64) -> i64 {
113 t.arena = arena
114 t.arena_len = arena_len
115 t.vhosts = vhosts
116 t.n_vhosts = 0
117 t.fallback_idx = -1
118 return 0
119}
120
121// Copy a null-terminated cstring into the arena starting at the
122// current write cursor (caller tracks the cursor via the returned
123// new length). Returns the offset where the string was written,
124// or -1 on overflow.
125func _vh_arena_push_cstr(t: *NxVHostTable, cursor: *i64, s: *u8) -> i64 {
126 let start: i64 = cursor[0]
127 var n: i64 = 0
128 while s[n] != 0 { n = n + 1 }
129 if start + n > t.arena_len { return -1 }
130 var i: i64 = 0
131 while i < n {
132 t.arena[start + i] = s[i]
133 i = i + 1
134 }
135 cursor[0] = start + n
136 return start
137}
138
139// Add a (host, doc_root) pair to the table. Writes both strings to
140// the arena. cursor must be a caller-owned i64 slot tracking arena
141// write position. Returns the new vhost index, or -1 on error.
142func nx_vhost_add(t: *NxVHostTable, cursor: *i64,
143 host: *u8, doc_root: *u8) -> i64 {
144 let h_off: i64 = _vh_arena_push_cstr(t, cursor, host)
145 if h_off < 0 { return -1 }
146 let r_off: i64 = _vh_arena_push_cstr(t, cursor, doc_root)
147 if r_off < 0 { return -1 }
148 let idx: i64 = t.n_vhosts
149 let base: i64 = idx * NX_VHOST_STRIDE
150 var host_len: i64 = 0
151 while host[host_len] != 0 { host_len = host_len + 1 }
152 var root_len: i64 = 0
153 while doc_root[root_len] != 0 { root_len = root_len + 1 }
154 t.vhosts[base + NX_VHOST_HNAME_OFF] = h_off
155 t.vhosts[base + NX_VHOST_HNAME_LEN] = host_len
156 t.vhosts[base + NX_VHOST_ROOT_OFF] = r_off
157 t.vhosts[base + NX_VHOST_ROOT_LEN] = root_len
158 t.n_vhosts = idx + 1
159 return idx
160}
161
162// Mark a vhost index as the fallback (catch-all). Passing -1 disables.
163func nx_vhost_set_fallback(t: *NxVHostTable, idx: i64) -> i64 {
164 if idx < -1 { return -1 }
165 if idx >= t.n_vhosts { return -1 }
166 t.fallback_idx = idx
167 return 0
168}
169
170// ===== Public: dispatch ===========================================
171//
172// Given a parsed HttpRequest, find the matching vhost and write the
173// doc-root range to *out_root_off / *out_root_len. Returns one of
174// the NX_VHOST_* verdict codes.
175
176func nx_vhost_dispatch(req_buf: *u8, req: *HttpRequest,
177 t: *NxVHostTable,
178 out_root_off: *i64,
179 out_root_len: *i64) -> i64 {
180 if (t as i64) == 0 { return NX_VHOST_BAD_ARG }
181 if t.n_vhosts < 0 { return NX_VHOST_BAD_ARG }
182
183 // Read the Host header value.
184 let host_off_slot: *i64 = sys_mmap(8) as *i64
185 let host_len_slot: *i64 = sys_mmap(8) as *i64
186 let has_host: i64 = http_get_header(req_buf, req, "Host" as *u8,
187 host_off_slot, host_len_slot)
188 if has_host != 1 {
189 // No Host header -- try fallback.
190 if t.fallback_idx >= 0 {
191 let fb_base: i64 = t.fallback_idx * NX_VHOST_STRIDE
192 out_root_off[0] = t.vhosts[fb_base + NX_VHOST_ROOT_OFF]
193 out_root_len[0] = t.vhosts[fb_base + NX_VHOST_ROOT_LEN]
194 return NX_VHOST_FALLBACK
195 }
196 out_root_off[0] = -1
197 out_root_len[0] = 0
198 return NX_VHOST_NO_HOST_HDR
199 }
200
201 let host_off: i64 = host_off_slot[0]
202 let host_len_raw: i64 = host_len_slot[0]
203 let host_len: i64 = _vh_strip_port(req_buf, host_off, host_len_raw)
204
205 // Linear scan -- vhost counts are tiny (single digits typical;
206 // sub-100 max). No hash table needed.
207 var i: i64 = 0
208 var found: i64 = -1
209 while i < t.n_vhosts {
210 let v_base: i64 = i * NX_VHOST_STRIDE
211 if _vh_eq_ci(req_buf, host_off, host_len,
212 t.arena,
213 t.vhosts[v_base + NX_VHOST_HNAME_OFF],
214 t.vhosts[v_base + NX_VHOST_HNAME_LEN]) == 1 {
215 found = i
216 i = t.n_vhosts // loop exit
217 }
218 if found < 0 { i = i + 1 }
219 }
220
221 if found >= 0 {
222 let f_base: i64 = found * NX_VHOST_STRIDE
223 out_root_off[0] = t.vhosts[f_base + NX_VHOST_ROOT_OFF]
224 out_root_len[0] = t.vhosts[f_base + NX_VHOST_ROOT_LEN]
225 return NX_VHOST_OK
226 }
227
228 // No explicit match; try fallback.
229 if t.fallback_idx >= 0 {
230 let fb_base: i64 = t.fallback_idx * NX_VHOST_STRIDE
231 out_root_off[0] = t.vhosts[fb_base + NX_VHOST_ROOT_OFF]
232 out_root_len[0] = t.vhosts[fb_base + NX_VHOST_ROOT_LEN]
233 return NX_VHOST_FALLBACK
234 }
235
236 out_root_off[0] = -1
237 out_root_len[0] = 0
238 return NX_VHOST_NO_MATCH
239}