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}