nx_doc_serve.nx source
↩ module page · 147 lines · 6317 B
1// nx_doc_serve.nx -- LEGAL RUNG D3: open-the-document-in-the-app-of-choice.
2//
3// module: nishi-core.legal.doc_serve
4// capability: LEGAL_DOC_SERVE
5//
6// Serves a stored document's ORIGINAL bytes so a client can open it in the app
7// of their choice -- inline in the browser (PDF/HTML/text) or downloaded to open
8// in a desktop app (Word/etc). A pure bytes-in/bytes-out HTTP response builder
9// (the proven nx_status_daemon handler pattern): a thin socket loop can wrap it,
10// but the logic + the gate need no socket.
11//
12// The s-class properties (each tested with a negative control in the gate):
13// BYTE-EXACT INTEGRITY -- the served body is bit-identical to the stored
14// bytes. For a legal instrument a single altered byte
15// is a DIFFERENT document, so the body is Content-
16// Length framed (never transcoded, never delimiter-
17// scanned); a body that itself contains CRLFCRLF / NUL
18// round-trips exactly.
19// HEADER-INJECTION SAFE -- the attachment filename is sanitized (control chars
20// stripped) so a malicious "name\r\nSet-Cookie: ..."
21// cannot inject a response header.
22// SAFE-BY-DEFAULT TYPE -- unknown formats are downloaded as octet-stream
23// (never inlined), and X-Content-Type-Options: nosniff
24// stops the browser MIME-sniffing a legal document.
25// PER-TENANT -- the serve layer reads only from the tenant's vault
26// prefix (nx_vault_valid_tid -- no cross-tenant).
27//
28// Composes: nx_doc_vault (D1 tenant-id discipline). license_tier: ORIGINAL
29// lineage_id: nishi_doc_serve_d3
30import "nx_syscalls.nx"
31
32// ---- document formats ----
33const DF_PDF: i64 = 0
34const DF_DOCX: i64 = 1
35const DF_HTML: i64 = 2
36const DF_TXT: i64 = 3
37const DF_NXDOC: i64 = 4 // our own sovereign document format
38const DF_OCTET: i64 = 5 // unknown -> download as octet-stream
39
40// ---- content-disposition ----
41const DISP_INLINE: i64 = 0 // open in the browser
42const DISP_ATTACHMENT: i64 = 1 // download -> open in the app of choice
43
44// ---- MIME type for a format (drives which app opens it). ----
45func nx_doc_content_type(fmt: i64) -> *u8 {
46 if fmt == DF_PDF { return "application/pdf" }
47 if fmt == DF_DOCX { return "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }
48 if fmt == DF_HTML { return "text/html; charset=utf-8" }
49 if fmt == DF_TXT { return "text/plain; charset=utf-8" }
50 if fmt == DF_NXDOC { return "application/x-nishi-doc" }
51 return "application/octet-stream"
52}
53
54// ---- the safe default disposition for a format. ----
55// Word docs + unknown formats download (open in the desktop app / never inline an
56// unknown type); browser-renderable formats open inline.
57func nx_doc_default_disposition(fmt: i64) -> i64 {
58 if fmt == DF_OCTET { return DISP_ATTACHMENT }
59 if fmt == DF_DOCX { return DISP_ATTACHMENT }
60 return DISP_INLINE
61}
62
63func ds_cat(out: *u8, o: i64, s: *u8) -> i64 { var k: i64 = 0; while s[k] != (0 as u8) { out[o] = s[k]; o = o + 1; k = k + 1 } return o }
64func ds_num(out: *u8, o: i64, v: i64) -> i64 {
65 if v == 0 { out[o] = 48; return o + 1 }
66 let t: *u8 = sys_mmap(28); var m: i64 = v; var k: i64 = 0
67 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
68 var i: i64 = 0; while i < k { out[o] = t[k - 1 - i]; o = o + 1; i = i + 1 }
69 return o
70}
71
72// ---- build a complete HTTP/1.1 200 response that serves doc_bytes EXACTLY. ----
73// Content-Length framed (byte-exact, injection-safe body); correct Content-Type
74// for "open in app of choice"; sanitized attachment filename. Returns total length.
75func nx_doc_serve_response(out: *u8, doc_bytes: *u8, doc_len: i64, fmt: i64,
76 disposition: i64, filename: *u8) -> i64 {
77 var o: i64 = 0
78 o = ds_cat(out, o, "HTTP/1.1 200 OK\r\n")
79 o = ds_cat(out, o, "Content-Type: ")
80 o = ds_cat(out, o, nx_doc_content_type(fmt))
81 o = ds_cat(out, o, "\r\n")
82 o = ds_cat(out, o, "Content-Disposition: ")
83 if disposition == DISP_ATTACHMENT {
84 o = ds_cat(out, o, "attachment; filename=\"")
85 // sanitize: strip control chars (<32) + the quote char so the filename
86 // can never inject a CRLF header or break the quoted-string.
87 var fi: i64 = 0
88 while filename[fi] != (0 as u8) {
89 let c: i64 = filename[fi] as i64
90 if c >= 32 { if c != 34 { out[o] = filename[fi]; o = o + 1 } }
91 fi = fi + 1
92 }
93 o = ds_cat(out, o, "\"")
94 } else {
95 o = ds_cat(out, o, "inline")
96 }
97 o = ds_cat(out, o, "\r\n")
98 o = ds_cat(out, o, "X-Content-Type-Options: nosniff\r\n")
99 o = ds_cat(out, o, "Content-Length: ")
100 o = ds_num(out, o, doc_len)
101 o = ds_cat(out, o, "\r\n\r\n")
102 var i: i64 = 0
103 while i < doc_len { out[o] = doc_bytes[i]; o = o + 1; i = i + 1 }
104 return o
105}
106
107// ---- find the header/body boundary (first CRLFCRLF); returns body offset, or -1. ----
108func ds_body_off(resp: *u8, n: i64) -> i64 {
109 var i: i64 = 0
110 while i + 3 < n {
111 if resp[i] == 13 { if resp[i+1] == 10 { if resp[i+2] == 13 { if resp[i+3] == 10 { return i + 4 } } } }
112 i = i + 1
113 }
114 return 0 - 1
115}
116
117// ---- the Content-Length-framed body equals orig[0..orig_len], byte for byte. ----
118func nx_doc_serve_roundtrip_ok(resp: *u8, n: i64, orig: *u8, orig_len: i64) -> i64 {
119 let bo: i64 = ds_body_off(resp, n)
120 if bo < 0 { return 0 }
121 if n - bo != orig_len { return 0 }
122 var i: i64 = 0
123 while i < orig_len {
124 if resp[bo + i] != orig[i] { return 0 }
125 i = i + 1
126 }
127 return 1
128}
129
130// ---- does resp[0..n] contain needle (substring)? (for injection neg-controls). ----
131func nx_doc_serve_contains(resp: *u8, n: i64, needle: *u8) -> i64 {
132 var nl: i64 = 0
133 while needle[nl] != (0 as u8) { nl = nl + 1 }
134 if nl == 0 { return 1 }
135 var i: i64 = 0
136 while i + nl <= n {
137 var k: i64 = 0
138 var hit: i64 = 1
139 while k < nl {
140 if resp[i + k] != needle[k] { hit = 0; break }
141 k = k + 1
142 }
143 if hit == 1 { return 1 }
144 i = i + 1
145 }
146 return 0
147}