code wiki / (root) / nx_h2_serve.nx

nx_h2_serve.nx source

↩ module page · 248 lines · 13338 B

1// nx_h2_serve.nx -- TUTOR-BOOTSTRAP SCAFFOLD (Claude, authored under the B1 2// rung(5) connection-driver LIFT of the R4-H2 HTTP/2-transport ladder), NOT 3// credited as team self-authoring (back-fill via the emitter-of-emitters once 4// the daemon-from-spec organ exists -- X-AUT-006c/e/f). 5// 6// THE REUSABLE ALPN-h2 SERVER CONNECTION-DRIVER organ (single responsibility: 7// SERVE ONE already-accepted h2 connection over OUR sovereign TLS 1.3). This is 8// the VERBATIM LIFT of _h2_serve_curl_daemon.nx's PROVEN hcd_run_server (renamed 9// h2_serve_connection, the param scfd -> ss_fd) -- it is NOT a rewrite (BL-012 10// compose-not-duplicate): the loop already serves an EXTERNAL curl --http2 GET 11// at http_version=2 response_code=200 byte-exact body (knowledge/status/ 12// h2_nx_h2_serve_curl.log). The daemon scaffold now SHRINKS to import this organ 13// and call the lifted driver in a counted accept loop. 14// 15// WHY A NEW ORGAN (rule 9 single-responsibility + rule 15 DRY, not nx_h2_server): 16// nx_h2_server.nx is the PURE in-memory FRAMER (h2_decode_request / 17// h2_server_send_response); its own header (lines 33-35) says it deliberately 18// does NOT import the tls13 record/session transport because "the in-memory 19// gate needs only the framing". The connection DRIVER is a DIFFERENT 20// responsibility: it owns the live TLS-over-socket transport loop (send our 21// SETTINGS -> read-record loop accumulating frames -> ACK client SETTINGS -> 22// parse HEADERS -> respond HEADERS 0x88 + DATA + GOAWAY -> shutdown(SHUT_WR) + 23// drain). Folding transport into nx_h2_server would force its framing gate to 24// import the whole tls13 server-session stack and collapse "framing AND 25// live-serving" into one organ (the 'and' split-test fails). So this organ 26// COMPOSES nx_h2_server for framing + the tls server transport stones. 27// 28// DRIVER CONTRACT (verbatim -- NO new return semantics): 29// h2_serve_connection(ss_fd, srand, sxpriv, cert_der, cert_len, ed_priv, 30// body, blen) -> i64 31// The session is established INSIDE via 32// nx_tls13_server_session_run_h2_ed25519(ss_fd, ...) (handshake inputs are 33// caller args; the established *Tls13ServerSession is NOT a caller arg -- 34// the caller hands an already-ACCEPTED fd + the per-connection handshake 35// entropy, single-responsibility). Returns 0 on a clean serve (handshake 36// h2 + parsed GET + wrote :status 200 + graceful FIN), or the SAME honest 37// negative codes the proven driver returns: handshake <=0 propagated; 38// -50/-51 record/recv; -53 non-GET; -57/-58 send; -60..-65 settings/ack/ 39// budget; -100 bad preface. 40// 41// FOUNDED ON (composes, each imported EXACTLY ONCE -- RC6 double-import is BENIGN 42// here and VERIFIED: nx_tls13_server_session_run_h2.nx ALREADY imports 43// nx_syscalls.nx AND nx_tls13_read_record_from_fd.nx (its lines 38,41) and the 44// proven daemon ALSO imports both directly yet builds+runs GREEN -- the build's 45// include-guard makes repeat imports idempotent, no duplicate-symbol): 46// - nx_syscalls.nx: sys_socket/bind/listen/accept/shutdown/read/write/mmap 47// (sys_accept pulled transitively from nx_syscalls_x86_64.nx, sys_shutdown 48// at nx_syscalls.nx:514). 49// - nx_h2_server.nx: framing -- transitively splices nx_h2_conformance -> 50// nx_h2_frame (h2_frame_write_settings_empty/_settings_ack/_headers/_data/ 51// _goaway, h2_check_preface, h2_frame_read_header) / nx_hpack / nx_h2_flow / 52// nx_h2_stream -- plus h2_decode_request + H2_METHOD_GET defined directly in 53// nx_h2_server.nx. 54// - nx_tls13_server_session_run_h2.nx: nx_tls13_server_session_run_h2_ed25519 55// + Tls13ServerSession (the ALPN-h2 Ed25519 handshake). 56// - nx_tls13_server_session_app_data.nx: app_send / app_recv. 57// - nx_tls13_read_record_from_fd.nx: the one-record reader. 58// The driver does NOT need nx_base64/x509/csprng/x25519/ed25519 -- those are 59// CERT-MINTING + PEM-EXPORT concerns that STAY in the daemon scaffold (the daemon 60// owns key/cert/socket lifecycle; this organ serves ONE connection). 61// 62// license_tier: INDEPENDENT_REDERIVE 63// genealogy_id: international-research-sources/ietf/rfc_9113 + rfc_7541 + rfc_8446 + rfc_7301 + rfc_5280 64// lineage_id: nishi_h2_serve_b1r5 65 66import "nx_syscalls.nx" 67import "nx_h2_server.nx" 68import "nx_tls13_server_session_run_h2.nx" 69import "nx_tls13_server_session_app_data.nx" 70import "nx_tls13_read_record_from_fd.nx" 71const K_MAGIC_16384: i64 = 16384 72const K_MAGIC_16645: i64 = 16645 73const K_MAGIC_2048: i64 = 2048 74 75// ---- the sovereign ALPN-h2 SERVER connection-driver (ONE already-accepted 76// connection) -- VERBATIM LIFT of _h2_serve_curl_daemon.nx hcd_run_server 77// (renamed h2_serve_connection; the param scfd -> ss_fd; every internal 78// socket-fd reference renamed scfd -> ss_fd consistently -- a pure rename, 79// the logic is byte-for-byte the PROVEN driver). ---- 80// Handshake (SELECT "h2" via emit_ee_alpn + Ed25519 CV), then run the REAL h2 81// server connection preamble: send our SETTINGS, read TLS records in a LOOP 82// accumulating h2 frames (curl sends preface+SETTINGS+WINDOW_UPDATE first and 83// the request HEADERS in a LATER record, awaiting the server's SETTINGS), ACK 84// curl's SETTINGS, parse the request HEADERS (:method/:path) when it arrives, 85// send HEADERS(:status 200)+DATA(END_STREAM)+GOAWAY on curl's stream, then 86// shutdown(SHUT_WR)+drain for a graceful FIN (close-with-unread-data was RSTing 87// curl's read of the response). Returns 0 ok; a NAMED negative code on failure 88// (never fabricates a method). VERIFIED LIVE: curl 8.5.0 --http2 --cacert -> 89// http_version=2 response_code=200 + byte-exact body, through OUR 100%-sovereign 90// server (no nghttp2/openssl/gcc on our side; curl = last-mile interop peer). 91func h2_serve_connection( 92 ss_fd: i64, srand: *u8, sxpriv: *u8, 93 cert_der: *u8, cert_len: i64, ed_priv: *u8, 94 body: *u8, blen: i64 95) -> i64 { 96 let sret: i64 = nx_tls13_server_session_run_h2_ed25519( 97 ss_fd, srand, sxpriv, cert_der, cert_len, ed_priv) 98 if sret <= 0 { return sret } // handshake failed -> propagate honest <0 99 let ss: *Tls13ServerSession = sret as *Tls13ServerSession 100 101 // ---- 1. send server SETTINGS (empty) immediately. A real h2 server sends 102 // its SETTINGS as the first frame; curl waits for it before sending 103 // its request HEADERS (this is why the one-record loopback read 104 // failed: curl's first record is only preface+SETTINGS+WINDOW_UPDATE, 105 // the HEADERS arrive in a LATER record). ---- 106 let sbuf: *u8 = sys_mmap(64) 107 let sbn: i64 = h2_frame_write_settings_empty(sbuf, 0) 108 let sorec: *u8 = sys_mmap(sbn + 64) 109 let ssn: i64 = nx_tls13_server_session_app_send(ss, sbuf, sbn, sorec, sbn + 64) 110 if ssn < 0 { return 0 - 60 } 111 var sw: i64 = 0 112 while sw < ssn { 113 let sk: i64 = sys_write(ss_fd, (sorec as i64 + sw) as *u8, ssn - sw) 114 if sk <= 0 { return 0 - 61 } 115 sw = sw + sk 116 } 117 118 // ---- 2. read-loop: accumulate decrypted h2 frames across TLS records, ACK 119 // curl's SETTINGS, and stop on the FIRST HEADERS frame (the request). 120 // This is the REAL h2 server connection preamble vs the loopback's 121 // one-shot single-record assumption. ---- 122 let frames: *u8 = sys_mmap(K_MAGIC_16384) 123 var flen: i64 = 0 124 var preface_ok: i64 = 0 125 var acked: i64 = 0 126 var got_headers: i64 = 0 127 var req_sid: i64 = 1 128 let m: *i64 = sys_mmap(8) as *i64 129 let po: *i64 = sys_mmap(8) as *i64 130 let pl: *i64 = sys_mmap(8) as *i64 131 let rec: *u8 = sys_mmap(K_MAGIC_16645) 132 let ob: *u8 = sys_mmap(K_MAGIC_16645) 133 let rlen: *i64 = sys_mmap(8) as *i64 134 let rtype: *i64 = sys_mmap(8) as *i64 135 let rflags: *i64 = sys_mmap(8) as *i64 136 let rsid: *i64 = sys_mmap(8) as *i64 137 var budget: i64 = 0 138 while got_headers == 0 { 139 if budget >= 64 { return 0 - 62 } // honest record-count cap 140 budget = budget + 1 141 let rtot: i64 = nx_tls13_read_record_from_fd(ss_fd, rec, K_MAGIC_16645) 142 if rtot <= 0 { return 0 - 50 } 143 let on: i64 = nx_tls13_server_session_app_recv(ss, rec, rtot, ob, K_MAGIC_16645) 144 if on < 0 { return 0 - 51 } 145 var ai: i64 = 0 146 while ai < on { 147 if flen >= K_MAGIC_16384 { return 0 - 63 } 148 frames[flen] = ob[ai] 149 flen = flen + 1 150 ai = ai + 1 151 } 152 if preface_ok == 0 { 153 if flen >= 24 { 154 if h2_check_preface(frames, 0, 24) != 1 { return 0 - 100 } 155 preface_ok = 1 156 } 157 } 158 if preface_ok == 1 { 159 var cur: i64 = 24 160 while cur < flen { 161 let pstart: i64 = h2_frame_read_header(frames, cur, flen, rlen, rtype, rflags, rsid) 162 if pstart < 0 { cur = cur; break } // incomplete header -> need more 163 let plen: i64 = rlen[0] 164 if pstart + plen > flen { cur = cur; break } // payload not fully arrived 165 let ftype: i64 = rtype[0] 166 let fflags: i64 = rflags[0] 167 let fsid: i64 = rsid[0] 168 if ftype == 0x04 { // SETTINGS 169 if (fflags & 0x01) == 0 { // not an ACK -> ACK curl's settings once 170 if acked == 0 { 171 let abuf: *u8 = sys_mmap(32) 172 let abn: i64 = h2_frame_write_settings_ack(abuf, 0) 173 let aorec: *u8 = sys_mmap(abn + 64) 174 let asn: i64 = nx_tls13_server_session_app_send(ss, abuf, abn, aorec, abn + 64) 175 if asn < 0 { return 0 - 64 } 176 var aw: i64 = 0 177 while aw < asn { 178 let ak: i64 = sys_write(ss_fd, (aorec as i64 + aw) as *u8, asn - aw) 179 if ak <= 0 { return 0 - 65 } 180 aw = aw + ak 181 } 182 acked = 1 183 } 184 } 185 } 186 if ftype == 0x01 { // HEADERS = the request 187 let dr: i64 = h2_decode_request(frames, pstart, pstart + plen, m, po, pl) 188 if dr < 0 { return dr } // honest propagate (no fabrication) 189 req_sid = fsid 190 got_headers = 1 191 } 192 cur = pstart + plen 193 if got_headers == 1 { cur = flen; break } 194 } 195 } 196 } 197 if m[0] != H2_METHOD_GET { return 0 - 53 } 198 199 // ---- 3. respond on curl's stream: HEADERS(:status 200) + DATA(END_STREAM) 200 // + GOAWAY(NO_ERROR). The GOAWAY tells curl the connection is closing 201 // gracefully so it does not try to keep it alive for reuse. ---- 202 let resp: *u8 = sys_mmap(512) 203 let hblk: *u8 = sys_mmap(8); hblk[0] = 0x88 as u8 // HPACK indexed :status 200 204 var ro: i64 = h2_frame_write_headers(resp, 0, req_sid, 0x04, hblk, 1) // END_HEADERS 205 if ro < 0 { return ro } 206 ro = h2_frame_write_data(resp, ro, req_sid, 0x01, body, blen) // DATA END_STREAM 207 if ro < 0 { return ro } 208 let nodbg: *u8 = sys_mmap(8) 209 ro = h2_frame_write_goaway(resp, ro, req_sid, 0x00, nodbg, 0) // GOAWAY NO_ERROR 210 if ro < 0 { return ro } 211 let outrec: *u8 = sys_mmap(ro + 64) 212 let sn: i64 = nx_tls13_server_session_app_send(ss, resp, ro, outrec, ro + 64) 213 if sn < 0 { return 0 - 57 } 214 var w: i64 = 0 215 while w < sn { 216 let k: i64 = sys_write(ss_fd, (outrec as i64 + w) as *u8, sn - w) 217 if k <= 0 { return 0 - 58 } 218 w = w + k 219 } 220 221 // ---- 4. GRACEFUL close: shutdown(SHUT_WR) sends a FIN so curl receives the 222 // FULL response before teardown, then drain curl's remaining inbound 223 // bytes (its SETTINGS-ACK / WINDOW_UPDATE / FIN) to EOF so the final 224 // close() is a clean FIN -- NOT a RST. (close() with unread RX data 225 // was sending a TCP RST that reset curl's read of the response: 226 // "Recv failure: Connection reset by peer".) ---- 227 sys_shutdown(ss_fd, 1) // 1 = SHUT_WR (Linux) 228 let dbuf: *u8 = sys_mmap(K_MAGIC_2048) 229 var dn: i64 = 1 230 var dc: i64 = 0 231 while dn > 0 { 232 if dc >= 64 { dn = 0 } // safety cap on drain reads 233 dc = dc + 1 234 let r: i64 = sys_read(ss_fd, dbuf, K_MAGIC_2048) 235 if r <= 0 { dn = 0 } // EOF (curl closed) or error -> done 236 } 237 return 0 238} 239 240// ---- self-gate main(): this organ is primarily a LIBRARY (the real proof is the 241// daemon run + external curl). main() just prints a scaffold-label line and 242// exits 0 so the organ BUILDS standalone (no sockets stood up here -- a 243// bind/accept self-test belongs to the daemon, single-responsibility). ---- 244func main() -> i64 { 245 sys_write(1, "nx_h2_serve: SCAFFOLD library -- h2_serve_connection lifted (B1 rung 5), proof = daemon + external curl\n" as *u8, 102) 246 sys_exit(0) 247 return 0 248}