code wiki / (root) / nx_ipp_transport.nx

nx_ipp_transport.nx source

↩ module page · 287 lines · 12141 B

1// nx_ipp_transport.nx -- sovereign IPP-over-HTTP transport (R2): talk to ANY real IPP printer. 2// 3// IPP operations ride on HTTP POST with Content-Type: application/ipp to the printer's IPP endpoint 4// (cleartext on TCP 631). This builds a Get-Printer-Attributes request with the R0 codec, POSTs it over 5// a raw sovereign TCP socket (reusing the nx_health_probe connect+timeout pattern), reads the reply with 6// a BOUNDED timeout (graceful #14 -- never hangs), strips the HTTP framing, de-chunks if needed 7// (nx_http_dechunk), and hands the IPP response body back for nx_printer_health to diagnose. 8// 9// NEVER-BRICK (#26): this only issues a READ-ONLY Get-Printer-Attributes query. It writes no job, no 10// setting, no firmware. Safe by construction. 11// no_silent_failure: every failure path returns a distinct sealed verdict (conn fail / no response / 12// bad http / dechunk fail / truncated) -- a dead or misbehaving printer is reported, never hidden. 13// Sovereign: own TCP + own HTTP framing; nx_cc -> nxasm, no curl/wget/gcc. 14// 15// genealogy_id: project-printer-management-ipp-sclass-2026-06-20 16// license_tier: ORIGINAL 17 18import "nx_syscalls.nx" 19import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 20import "nx_ipp_codec.nx" 21import "nx_http_dechunk.nx" 22const NX_MAGIC_2048: i64 = 2048 23const NX_MAGIC_4096: i64 = 4096 24const NX_MAGIC_1024: i64 = 1024 25 26// ---- sealed transport verdict (no_silent_failure) ----------------- 27const NX_IPT_OK: i64 = 1 28const NX_IPT_CONN_FAIL: i64 = 2 // socket/connect failed -> printer down / unreachable 29const NX_IPT_NO_RESP: i64 = 3 // connected but nothing back within timeout -> hung 30const NX_IPT_BAD_HTTP: i64 = 4 // reply had no HTTP header terminator -> garbage / wrong service 31const NX_IPT_DECHUNK_FAIL: i64 = 5 // chunked body malformed 32const NX_IPT_TRUNC: i64 = 6 // body did not fit the caller buffer 33const NX_IPT_VERDICT_N: i64 = 7 34 35func nx_ipt_verdict_name(v: i64) -> *u8 { 36 if v == NX_IPT_OK { return "OK" as *u8 } 37 if v == NX_IPT_CONN_FAIL { return "CONN_FAIL(down/unreachable)" as *u8 } 38 if v == NX_IPT_NO_RESP { return "NO_RESP(hung)" as *u8 } 39 if v == NX_IPT_BAD_HTTP { return "BAD_HTTP" as *u8 } 40 if v == NX_IPT_DECHUNK_FAIL { return "DECHUNK_FAIL" as *u8 } 41 if v == NX_IPT_TRUNC { return "TRUNCATED" as *u8 } 42 return "UNKNOWN" as *u8 43} 44 45// ---- small builders ----------------------------------------------- 46 47func nx_ipt_sockaddr(buf: *u8, a: i64, b: i64, c: i64, d: i64, port: i64) -> i64 { 48 var i: i64 = 0 49 while i < 16 { buf[i] = 0 as u8; i = i + 1 } 50 buf[0] = 2 as u8 // AF_INET 51 buf[2] = ((port >> 8) & 0xff) as u8 52 buf[3] = (port & 0xff) as u8 53 buf[4] = a as u8; buf[5] = b as u8; buf[6] = c as u8; buf[7] = d as u8 54 return 0 55} 56 57func nx_ipt_puts(buf: *u8, off: i64, s: *u8) -> i64 { 58 var o: i64 = off 59 var i: i64 = 0 60 while s[i] != (0 as u8) { buf[o] = s[i]; o = o + 1; i = i + 1 } 61 return o 62} 63 64func nx_ipt_puti(buf: *u8, off: i64, v: i64) -> i64 { 65 var o: i64 = off 66 if v == 0 { buf[o] = 48 as u8; return o + 1 } 67 var m: i64 = v 68 let tmp: *u8 = sys_mmap(24) 69 var k: i64 = 0 70 while m > 0 { tmp[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 71 var i: i64 = k - 1 72 while i >= 0 { buf[o] = tmp[i]; o = o + 1; i = i - 1 } 73 return o 74} 75 76func nx_ipt_lc(ch: i64) -> i64 { 77 if ch >= 65 { if ch <= 90 { return ch + 32 } } 78 return ch 79} 80 81// case-insensitive substring offset of null-terminated `pat` in buf[0..n), or -1. 82func nx_ipt_ci_find(buf: *u8, n: i64, pat: *u8) -> i64 { 83 let pl: i64 = nx_ipp_strlen(pat) 84 if pl == 0 { return 0 } 85 if pl > n { return 0 - 1 } 86 var i: i64 = 0 87 let end: i64 = n - pl + 1 88 var hit: i64 = 0 - 1 89 while i < end { 90 if hit < 0 { 91 var j: i64 = 0 92 var ok: i64 = 1 93 while j < pl { 94 if nx_ipt_lc(buf[i + j] as i64) != nx_ipt_lc(pat[j] as i64) { ok = 0; j = pl } 95 else { j = j + 1 } 96 } 97 if ok == 1 { hit = i } 98 } 99 i = i + 1 100 } 101 return hit 102} 103 104// parse the numeric status code from an HTTP status line ("HTTP/1.1 200 OK" -> 200), or -1. 105func nx_ipt_http_status(buf: *u8, n: i64) -> i64 { 106 var i: i64 = 0 107 while i < n { if buf[i] == (32 as u8) { i = i + 1; break } i = i + 1 } // first space 108 var code: i64 = 0 109 var cnt: i64 = 0 110 var keep: i64 = 1 111 while keep == 1 { 112 if i >= n { keep = 0 } 113 else { 114 let ch: i64 = buf[i] as i64 115 if ch < 48 { keep = 0 } 116 else { 117 if ch > 57 { keep = 0 } 118 else { code = code * 10 + (ch - 48); cnt = cnt + 1; i = i + 1 } 119 } 120 } 121 } 122 if cnt == 0 { return 0 - 1 } 123 return code 124} 125 126// Build the IPP Get-Printer-Attributes request for printer-uri ipp://a.b.c.d<path> into req. Returns len. 127func nx_ipt_build_request(req: *u8, a: i64, b: i64, c: i64, d: i64, path: *u8) -> i64 { 128 let uri: *u8 = sys_mmap(256) 129 var uo: i64 = 0 130 uo = nx_ipt_puts(uri, uo, "ipp://") 131 uo = nx_ipt_puti(uri, uo, a); uri[uo] = 46 as u8; uo = uo + 1 132 uo = nx_ipt_puti(uri, uo, b); uri[uo] = 46 as u8; uo = uo + 1 133 uo = nx_ipt_puti(uri, uo, c); uri[uo] = 46 as u8; uo = uo + 1 134 uo = nx_ipt_puti(uri, uo, d) 135 uo = nx_ipt_puts(uri, uo, path) 136 uri[uo] = 0 as u8 137 138 var r: i64 = nx_ipp_begin(req, 1, 1, NX_IPP_OP_GET_PRINTER_ATTRIBUTES, 1) 139 r = nx_ipp_group(req, r, NX_IPP_GRP_OPERATION) 140 r = nx_ipp_attr(req, r, NX_IPP_VT_CHARSET, "attributes-charset", 18, "utf-8", 5) 141 r = nx_ipp_attr(req, r, NX_IPP_VT_NATLANG, "attributes-natural-language", 27, "en", 2) 142 r = nx_ipp_attr(req, r, NX_IPP_VT_URI, "printer-uri", 11, uri, uo) 143 r = nx_ipp_end(req, r) 144 return r 145} 146 147// Strip HTTP framing from a complete response in resp_buf[0..total): set out2[0]=HTTP status, extract the 148// (de-chunked) IPP body into body_buf with out2[1]=len. PURE (no I/O) -> unit-testable offline. Sealed verdict. 149func nx_ipt_extract_body(resp_buf: *u8, total: i64, body_buf: *u8, body_cap: i64, out2: *i64) -> i64 { 150 out2[0] = 0 151 out2[1] = 0 152 if total <= 0 { return NX_IPT_NO_RESP } 153 let hdr_end: i64 = nx_ipt_ci_find(resp_buf, total, "\r\n\r\n") 154 if hdr_end < 0 { return NX_IPT_BAD_HTTP } 155 out2[0] = nx_ipt_http_status(resp_buf, total) 156 let body_off: i64 = hdr_end + 4 157 let raw_len: i64 = total - body_off 158 let chunked: i64 = nx_ipt_ci_find(resp_buf, hdr_end, "transfer-encoding: chunked") 159 if chunked >= 0 { 160 let dl: i64 = nx_http_dechunk(((resp_buf as i64) + body_off) as *u8, raw_len, body_buf, body_cap) 161 if dl < 0 { return NX_IPT_DECHUNK_FAIL } 162 out2[1] = dl 163 } else { 164 if raw_len > body_cap { return NX_IPT_TRUNC } 165 var i: i64 = 0 166 while i < raw_len { body_buf[i] = resp_buf[body_off + i]; i = i + 1 } 167 out2[1] = raw_len 168 } 169 return NX_IPT_OK 170} 171 172// One-shot: query Get-Printer-Attributes from ip(a.b.c.d):port at `path`. Reads the HTTP reply into 173// resp_buf, extracts (de-chunking if needed) the IPP body into body_buf. out2[0]=HTTP status, 174// out2[1]=IPP body length. Returns a sealed verdict. 175func nx_ipt_query_printer(a: i64, b: i64, c: i64, d: i64, port: i64, path: *u8, 176 resp_buf: *u8, resp_cap: i64, body_buf: *u8, body_cap: i64, 177 timeout_sec: i64, out2: *i64) -> i64 { 178 out2[0] = 0 179 out2[1] = 0 180 181 // 1. IPP request 182 let ipp: *u8 = sys_mmap(NX_MAGIC_2048) 183 let ipp_len: i64 = nx_ipt_build_request(ipp, a, b, c, d, path) 184 185 // 2. HTTP POST framing (header + body) into a send buffer 186 let snd: *u8 = sys_mmap(NX_MAGIC_4096) 187 var so: i64 = 0 188 so = nx_ipt_puts(snd, so, "POST ") 189 so = nx_ipt_puts(snd, so, path) 190 so = nx_ipt_puts(snd, so, " HTTP/1.1\r\nHost: ") 191 so = nx_ipt_puti(snd, so, a); snd[so] = 46 as u8; so = so + 1 192 so = nx_ipt_puti(snd, so, b); snd[so] = 46 as u8; so = so + 1 193 so = nx_ipt_puti(snd, so, c); snd[so] = 46 as u8; so = so + 1 194 so = nx_ipt_puti(snd, so, d); snd[so] = 58 as u8; so = so + 1 // ':' 195 so = nx_ipt_puti(snd, so, port) 196 so = nx_ipt_puts(snd, so, "\r\nContent-Type: application/ipp\r\nContent-Length: ") 197 so = nx_ipt_puti(snd, so, ipp_len) 198 so = nx_ipt_puts(snd, so, "\r\nAccept: application/ipp\r\nConnection: close\r\n\r\n") 199 var bi: i64 = 0 200 while bi < ipp_len { snd[so] = ipp[bi]; so = so + 1; bi = bi + 1 } 201 202 // 3. connect + send + bounded recv 203 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 204 if fd < 0 { return NX_IPT_CONN_FAIL } 205 sys_set_socket_timeout(fd, timeout_sec) 206 let dest: *u8 = sys_mmap(16) 207 nx_ipt_sockaddr(dest, a, b, c, d, port) 208 if nx_connect_bounded(fd, dest, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return NX_IPT_CONN_FAIL } 209 sys_write(fd, snd, so) 210 211 var total: i64 = 0 212 var keep: i64 = 1 213 while keep == 1 { 214 if total >= resp_cap { keep = 0 } 215 else { 216 let r: i64 = sys_read(fd, ((resp_buf as i64) + total) as *u8, resp_cap - total) 217 if r <= 0 { keep = 0 } 218 else { total = total + r } 219 } 220 } 221 sys_close(fd) 222 // 4. strip HTTP framing + de-chunk (shared with the offline gate) 223 return nx_ipt_extract_body(resp_buf, total, body_buf, body_cap, out2) 224} 225 226// write all `len` bytes (loop over partial writes). Returns 0 ok, -1 on error. 227func nx_ipt_send_all(fd: i64, buf: *u8, len: i64) -> i64 { 228 var sent: i64 = 0 229 var keep: i64 = 1 230 var rc: i64 = 0 231 while keep == 1 { 232 if sent >= len { keep = 0 } 233 else { 234 let w: i64 = sys_write(fd, ((buf as i64) + sent) as *u8, len - sent) 235 if w <= 0 { rc = 0 - 1; keep = 0 } 236 else { sent = sent + w } 237 } 238 } 239 return rc 240} 241 242// POST an arbitrary application/ipp payload (an IPP request, optionally with document data appended). 243// Reads the reply, extracts the (de-chunked) IPP response body. out2[0]=HTTP status, out2[1]=IPP body len. 244// Sealed transport verdict. READ-ONLY w.r.t. firmware (never-brick) -- it only sends an IPP operation. 245func nx_ipt_post_ipp(a: i64, b: i64, c: i64, d: i64, port: i64, path: *u8, 246 payload: *u8, payload_len: i64, 247 resp_buf: *u8, resp_cap: i64, body_buf: *u8, body_cap: i64, 248 timeout_sec: i64, out2: *i64) -> i64 { 249 out2[0] = 0 250 out2[1] = 0 251 // HTTP request header 252 let hdr: *u8 = sys_mmap(NX_MAGIC_1024) 253 var so: i64 = 0 254 so = nx_ipt_puts(hdr, so, "POST ") 255 so = nx_ipt_puts(hdr, so, path) 256 so = nx_ipt_puts(hdr, so, " HTTP/1.1\r\nHost: ") 257 so = nx_ipt_puti(hdr, so, a); hdr[so] = 46 as u8; so = so + 1 258 so = nx_ipt_puti(hdr, so, b); hdr[so] = 46 as u8; so = so + 1 259 so = nx_ipt_puti(hdr, so, c); hdr[so] = 46 as u8; so = so + 1 260 so = nx_ipt_puti(hdr, so, d); hdr[so] = 58 as u8; so = so + 1 261 so = nx_ipt_puti(hdr, so, port) 262 so = nx_ipt_puts(hdr, so, "\r\nContent-Type: application/ipp\r\nContent-Length: ") 263 so = nx_ipt_puti(hdr, so, payload_len) 264 so = nx_ipt_puts(hdr, so, "\r\nAccept: application/ipp\r\nConnection: close\r\n\r\n") 265 266 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 267 if fd < 0 { return NX_IPT_CONN_FAIL } 268 sys_set_socket_timeout(fd, timeout_sec) 269 let dest: *u8 = sys_mmap(16) 270 nx_ipt_sockaddr(dest, a, b, c, d, port) 271 if nx_connect_bounded(fd, dest, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return NX_IPT_CONN_FAIL } 272 if nx_ipt_send_all(fd, hdr, so) != 0 { sys_close(fd); return NX_IPT_CONN_FAIL } 273 if nx_ipt_send_all(fd, payload, payload_len) != 0 { sys_close(fd); return NX_IPT_CONN_FAIL } 274 275 var total: i64 = 0 276 var keep: i64 = 1 277 while keep == 1 { 278 if total >= resp_cap { keep = 0 } 279 else { 280 let r: i64 = sys_read(fd, ((resp_buf as i64) + total) as *u8, resp_cap - total) 281 if r <= 0 { keep = 0 } 282 else { total = total + r } 283 } 284 } 285 sys_close(fd) 286 return nx_ipt_extract_body(resp_buf, total, body_buf, body_cap, out2) 287}