code wiki / (root) / nx_doc_serve.nx

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}