code wiki / (root) / nx_pages_vhost.nx

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}