code wiki / _hdl_build / nx_media_server.nx

nx_media_server.nx source

↩ module page · 948 lines · 65396 B

1// nx_media_server.nx -- SOVEREIGN media server (operator 2026-06-13: NO python, Nishi hardware-up). 2// Composes the PROVEN nx_http_server (socket/listen/accept/read/send) -- NO 3rd party. Serves the 3// /media browser + the FULL NAS library paginated (idx_*.wsl) + range-streamed files from the drvfs 4// NAS mounts (/mnt/nas_*). LAN-bound 0.0.0.0 (internal-network; operator-authorized 2026-06-16; PUBLIC-internet deferred to login-for-sites workstream). Fork-per-connection 5// for a concurrent grid. license_tier: ORIGINAL 6import "nx_syscalls.nx" 7import "nx_http_server.nx" 8import "nx_book_search_lib.nx" // SC20: bks_search ranked library search (BM25) 9import "_pdf_reflow_lib.nx" // R3b: PDF text layer -> reflowable mobile HTML (else fall back to page-images) 10import "nx_site_auth.nx" // AUTH: the one S-class gate (nx_sa_validate over X-Nishi-Session, no cookie) 11import "hub/nx_modern_auth_flow.nx" // AUTH: nx_modern_auth_login + nx_auth_context_init + nx_uas_server_keys_load_or_init 12import "nx_base64.nx" // AUTH: b64_encode the 152B session token 13import "nx_galx_authz.nx" // /media SFW allowlist: galx_media_path_ok denies the owner-only nas_logging (recordings/NSFW = /gallery only) 14import "nx_media_cover_lib.nx" // /api/cover: map a book path -> its EMBEDDED cover image on disk (extract-on-miss), shared with nx_media_cover_gate 15import "nx_token_counter.nx" // tok_estimate + pack_text: /api/llmpack packs a book into a token-budgeted LLM context ("push a book into an LLM natively") 16import "nx_reader_zoomserve.nx" // /api/zoomprep: intelligent-zoom prep (tile pyramid + panel map per comic page; tiles then ride /api/book) 17import "nx_reader_sync.nx" // R1 SYNC SPINE: /api/pos per-user position (Whispersync-class) + zero-JS auto-position on chapter fetches 18import "nx_reader_library.nx" // shared/personal areas: rl_slug_guard/rl_may_access (isolation) + rl_list_personal_json + rl_promote_to_shared 19import "nx_manga_add.nx" // POST /api/manga/add -> fork nx_manga_get into the caller's PRIVATE area 20import "nx_manga_search.nx" // SOURCE BROWSER: /api/msearch (on-site search) /api/mfeed (chapters) /api/thumb (cover proxy) 21import "nx_manga_shelf_lib.nx" // LOCAL SHELF: /api/shelf series->volumes catalog over the ALREADY-OWNED library (browse-before-download, local half) 22const MS_MAGIC_2000000: i64 = 2000000 23const MS_MAGIC_8192: i64 = 8192 24const MS_MAGIC_262144: i64 = 262144 25const MS_MAGIC_4096: i64 = 4096 26const MS_MAGIC_120000: i64 = 120000 27const MS_MAGIC_900000: i64 = 900000 28const MS_MAGIC_16777216: i64 = 16777216 29const MS_MAGIC_5000: i64 = 5000 30const MS_MAGIC_65536: i64 = 65536 31const MS_MAGIC_1469598103: i64 = 1469598103 32const MS_MAGIC_72057594037927931: i64 = 72057594037927931 33const MS_MAGIC_4194304: i64 = 4194304 34const MS_MAGIC_1024: i64 = 1024 35const MS_MAGIC_32768: i64 = 32768 36const MS_MAGIC_2097152: i64 = 2097152 37const MS_MAGIC_86400: i64 = 86400 38 39const MS_PORT: i64 = 8790 40const MS_BROWSER: *u8 = "knowledge/staging/media/browser.html" 41const MS_READER: *u8 = "knowledge/staging/media/reader.html" 42const MS_CBX: *u8 = "knowledge/staging/media/cbx.html" 43const MS_ZOOM: *u8 = "knowledge/staging/media/zoom.html" // organ-emitted by nx_reader_zoom_emit (guided-view tile zoom) 44const MS_MANGA: *u8 = "knowledge/staging/media/manga.html" // organ-emitted by nx_manga_ui_emit (add + personal library UI) 45const MS_SHELF: *u8 = "knowledge/staging/media/shelf.html" // organ-emitted by nx_manga_shelf_emit (LOCAL comics/manga series browse) 46const MS_HEALTH: *u8 = "knowledge/staging/media/health.html" // organ-emitted by nx_media_health_emit (media-liveness dashboard) 47const MS_HEALTH_JSON: *u8 = "knowledge/staging/media/mediahealth.json" // written by nx_movie_probe --sweep 48const MS_READER_ROOT: *u8 = "knowledge/staging/media/reader/" 49 50func ms_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 51func ms_puts(s: *u8) -> i64 { sys_write(1, s, ms_slen(s)); return 0 } 52func ms_app(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { dst[off+i] = src[i]; i = i + 1 } return off + n } 53func ms_apps(dst: *u8, off: i64, s: *u8) -> i64 { return ms_app(dst, off, s, ms_slen(s)) } 54func ms_dec(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8 = sys_mmap(28); var m: i64=v; var k: i64=0; if m==0 {t[0]=48;k=1}; while m>0 {t[k]=(48+(m%10)) as u8; m=m/10; k=k+1}; var i: i64=0; while i<k { dst[off+i]=t[k-1-i]; i=i+1 } return off+k } 55func ms_starts(p: *u8, plen: i64, pre: *u8) -> i64 { let m: i64 = ms_slen(pre); if m > plen { return 0 } var i: i64 = 0; while i < m { if p[i] != pre[i] { return 0 } i = i + 1 } return 1 } 56 57// extract ?key=value from a path/query into out (NUL-terminated); returns value length (0 if absent). 58func ms_qval(p: *u8, plen: i64, key: *u8, out: *u8, outcap: i64) -> i64 { 59 let kl: i64 = ms_slen(key) 60 var i: i64 = 0 61 while i + kl + 1 <= plen { 62 var hit: i64 = 1 63 var j: i64 = 0 64 while j < kl { if p[i+j] != key[j] { hit = 0; j = kl } else { j = j + 1 } } 65 if hit == 1 { if p[i+kl] == (61 as u8) { 66 var s: i64 = i + kl + 1 67 var o: i64 = 0 68 while s < plen { if p[s] == (38 as u8) { s = plen } else { if o < outcap - 1 { out[o] = p[s]; o = o + 1 } s = s + 1 } } 69 out[o] = 0 as u8 70 return o 71 } } 72 i = i + 1 73 } 74 out[0] = 0 as u8 75 return 0 76} 77 78// URL-decode src[0..slen] into dst (NUL-term); returns dst length. 79func ms_urldec(src: *u8, slen: i64, dst: *u8) -> i64 { 80 var i: i64 = 0 81 var o: i64 = 0 82 while i < slen { 83 let c: i64 = src[i] as i64 84 if c == 37 { if i + 2 < slen { 85 let h1: i64 = src[i+1] as i64 86 let h2: i64 = src[i+2] as i64 87 var v1: i64 = h1 - 48; if h1 >= 97 { v1 = h1 - 87 } else { if h1 >= 65 { v1 = h1 - 55 } } 88 var v2: i64 = h2 - 48; if h2 >= 97 { v2 = h2 - 87 } else { if h2 >= 65 { v2 = h2 - 55 } } 89 dst[o] = ((v1 * 16) + v2) as u8; o = o + 1; i = i + 3 90 } else { dst[o] = src[i]; o = o + 1; i = i + 1 } } 91 else { if c == 43 { dst[o] = 32 as u8 } else { dst[o] = src[i] } o = o + 1; i = i + 1 } 92 } 93 dst[o] = 0 as u8 94 return o 95} 96 97func ms_ctype(path: *u8) -> *u8 { 98 let n: i64 = ms_slen(path) 99 if n < 4 { return "application/octet-stream" as *u8 } 100 let a: i64 = path[n-3] as i64; let b: i64 = path[n-2] as i64; let c: i64 = path[n-1] as i64 101 if c == 103 { if b == 110 { return "image/png" as *u8 } if b == 112 { if a == 106 { return "image/jpeg" as *u8 } } if b == 102 { return "image/gif" as *u8 } } 102 if c == 112 { if b == 52 { return "video/mp4" as *u8 } } 103 if c == 118 { if b == 107 { return "video/x-matroska" as *u8 } if b == 109 { return "video/x-msvideo" as *u8 } } 104 if c == 102 { if b == 100 { return "application/pdf" as *u8 } } 105 if c == 108 { return "text/html; charset=utf-8" as *u8 } 106 return "application/octet-stream" as *u8 107} 108 109// map ?type= to its idx file path 110func ms_idxpath(t: *u8) -> *u8 { 111 if t[0] == (97 as u8) { return "knowledge/staging/media/idx_ai.wsl" as *u8 } // ai 112 if t[0] == (118 as u8) { return "knowledge/staging/media/idx_video.wsl" as *u8 } // video 113 if t[0] == (112 as u8) { return "knowledge/staging/media/idx_photo.wsl" as *u8 } // photo 114 if t[0] == (98 as u8) { return "knowledge/staging/media/idx_book.wsl" as *u8 } // book 115 if t[0] == (115 as u8) { return "knowledge/staging/media/idx_stl.wsl" as *u8 } // stl 116 return "knowledge/staging/media/idx_ai.wsl" as *u8 117} 118 119// /api/list?type=&offset=&limit= -> JSON {total, items:[{n,p}]} 120func ms_handle_list(cfd: i64, p: *u8, plen: i64) -> i64 { 121 let tb: *u8 = sys_mmap(16); ms_qval(p, plen, "type" as *u8, tb, 16) 122 if tb[0] == (0 as u8) { tb[0] = 97 as u8; tb[1] = 0 as u8 } 123 let ob: *u8 = sys_mmap(16); ms_qval(p, plen, "offset" as *u8, ob, 16) 124 var off: i64 = 0; var oi: i64 = 0; while ob[oi] != (0 as u8) { off = off*10 + (ob[oi]-48); oi = oi + 1 } 125 let lim: i64 = 120 126 let lenbox: *i64 = sys_mmap(8) as *i64 127 let buf: *u8 = sys_read_file(ms_idxpath(tb), lenbox) 128 let blen: i64 = lenbox[0] 129 let out: *u8 = sys_mmap(MS_MAGIC_2000000) 130 var w: i64 = 0 131 w = ms_apps(out, w, "{\"items\":[" as *u8) 132 var line: i64 = 0 133 var i: i64 = 0 134 var emitted: i64 = 0 135 while i < blen { 136 let ls: i64 = i 137 var le: i64 = ls 138 var stop: i64 = 0 139 while stop == 0 { if le >= blen { stop = 1 } else { if buf[le] == (10 as u8) { stop = 1 } else { le = le + 1 } } } 140 var ce: i64 = le 141 if ce > ls { if buf[ce-1] == (13 as u8) { ce = ce - 1 } } 142 if ce > ls { 143 if line >= off { if emitted < lim { 144 var bn: i64 = ls 145 var x: i64 = ls 146 while x < ce { if buf[x] == (47 as u8) { bn = x + 1 } x = x + 1 } 147 if emitted > 0 { w = ms_apps(out, w, "," as *u8) } 148 w = ms_apps(out, w, "{\"n\":\"" as *u8) 149 var y: i64 = bn 150 while y < ce { let ch: i64 = buf[y] as i64; if ch == 34 { w = ms_apps(out, w, "\\\"" as *u8) } else { out[w] = buf[y]; w = w + 1 } y = y + 1 } 151 w = ms_apps(out, w, "\",\"p\":\"" as *u8) 152 y = ls 153 while y < ce { let ch: i64 = buf[y] as i64; if ch == 34 { w = ms_apps(out, w, "\\\"" as *u8) } else { out[w] = buf[y]; w = w + 1 } y = y + 1 } 154 w = ms_apps(out, w, "\"}" as *u8) 155 emitted = emitted + 1 156 } } 157 line = line + 1 158 } 159 i = le + 1 160 } 161 w = ms_apps(out, w, "],\"total\":" as *u8) 162 w = ms_dec(out, w, line) 163 w = ms_apps(out, w, "}" as *u8) 164 let hdr: *u8 = sys_mmap(256); var h: i64 = 0 165 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " as *u8) 166 h = ms_dec(hdr, h, w) 167 h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 168 sys_write(cfd, hdr, h) 169 sys_write(cfd, out, w) 170 sys_close(cfd) 171 return 0 172} 173 174// /file?p=<urlenc nas path> -> stream with Range 175func ms_handle_file(cfd: i64, p: *u8, plen: i64, req: *u8, reqn: i64) -> i64 { 176 let enc: *u8 = sys_mmap(MS_MAGIC_8192); let el: i64 = ms_qval(p, plen, "p" as *u8, enc, MS_MAGIC_8192) 177 let path: *u8 = sys_mmap(MS_MAGIC_8192); ms_urldec(enc, el, path) 178 // allowlist: a NAS path EXCEPT the owner-only recordings/NSFW (/mnt/nas_logging) -- /media is SFW-for-everyone 179 if galx_media_path_ok(path, ms_slen(path)) == 0 { let d: *u8 = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 180 let fd: i64 = sys_openat_rd(path) 181 if fd < 0 { let d: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 182 let sz: i64 = sys_lseek(fd, 0, 2) 183 sys_lseek(fd, 0, 0) 184 // parse Range: bytes=START- 185 var start: i64 = 0; var isrange: i64 = 0 186 var ri: i64 = 0 187 while ri + 6 < reqn { if req[ri]==(82 as u8) { if req[ri+1]==(97 as u8) { if req[ri+2]==(110 as u8) { if req[ri+3]==(103 as u8) { if req[ri+4]==(101 as u8) { if req[ri+5]==(58 as u8) { 188 var s: i64 = ri + 6; while s < reqn { if req[s] == (61 as u8) { s = s + 1; isrange = 1; while s < reqn { if req[s] >= 48 { if req[s] <= 57 { start = start*10 + (req[s]-48); s = s + 1 } else { s = reqn } } else { s = reqn } } } else { s = s + 1 } } 189 ri = reqn 190 } } } } } } ri = ri + 1 } 191 let hdr: *u8 = sys_mmap(512); var h: i64 = 0 192 let ct: *u8 = ms_ctype(path) 193 if isrange == 1 { 194 sys_lseek(fd, start, 0) 195 let endb: i64 = sz - 1; let length: i64 = sz - start 196 h = ms_apps(hdr, h, "HTTP/1.1 206 Partial Content\r\nContent-Type: " as *u8); h = ms_apps(hdr, h, ct) 197 h = ms_apps(hdr, h, "\r\nAccept-Ranges: bytes\r\nContent-Range: bytes " as *u8) 198 h = ms_dec(hdr, h, start); h = ms_apps(hdr, h, "-" as *u8); h = ms_dec(hdr, h, endb); h = ms_apps(hdr, h, "/" as *u8); h = ms_dec(hdr, h, sz) 199 h = ms_apps(hdr, h, "\r\nContent-Length: " as *u8); h = ms_dec(hdr, h, length); h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 200 } else { 201 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); h = ms_apps(hdr, h, ct) 202 h = ms_apps(hdr, h, "\r\nAccept-Ranges: bytes\r\nContent-Length: " as *u8); h = ms_dec(hdr, h, sz); h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 203 } 204 sys_write(cfd, hdr, h) 205 let chunk: *u8 = sys_mmap(MS_MAGIC_262144) 206 var go: i64 = 1 207 while go == 1 { let r: i64 = sys_read(fd, chunk, MS_MAGIC_262144); if r <= 0 { go = 0 } else { sys_write(cfd, chunk, r) } } 208 sys_close(fd); sys_close(cfd) 209 return 0 210} 211 212// 1 if hay[0..hl] contains the needle (used to reject ".." path traversal) 213func ms_contains(hay: *u8, hl: i64, needle: *u8) -> i64 { 214 let nl: i64 = ms_slen(needle) 215 if nl == 0 { return 0 } 216 var i: i64 = 0 217 while i + nl <= hl { 218 var k: i64 = 0; var hit: i64 = 1 219 while k < nl { if hay[i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } } 220 if hit == 1 { return 1 } 221 i = i + 1 222 } 223 return 0 224} 225 226// serve a static text/html file at a fixed repo path (clone of ms_serve_browser, path-parameterized) 227func ms_serve_static(cfd: i64, path: *u8, ctype: *u8) -> i64 { 228 let lenbox: *i64 = sys_mmap(8) as *i64 229 let body: *u8 = sys_read_file(path, lenbox) 230 let blen: i64 = lenbox[0] 231 if (body as i64) == 0 { let d: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 232 let hdr: *u8 = sys_mmap(256); var h: i64 = 0 233 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); h = ms_apps(hdr, h, ctype) 234 h = ms_apps(hdr, h, "\r\nCache-Control: no-store\r\nContent-Length: " as *u8) 235 h = ms_dec(hdr, h, blen); h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 236 sys_write(cfd, hdr, h) 237 if blen > 0 { sys_write(cfd, body, blen) } 238 sys_close(cfd) 239 return 0 240} 241 242// /api/book?p=<urlenc relpath under knowledge/staging/media/reader/> -> serve the reader model/text. 243// ALLOWLIST: caller-supplied p is prefixed with MS_READER_ROOT here, so any path escaping the reader 244// dir requires "..". We reject "../" defensively. .json -> application/json; else text/plain utf-8. 245func ms_handle_book(cfd: i64, p: *u8, plen: i64) -> i64 { 246 let enc: *u8 = sys_mmap(MS_MAGIC_4096); let el: i64 = ms_qval(p, plen, "p" as *u8, enc, MS_MAGIC_4096) 247 let rel: *u8 = sys_mmap(MS_MAGIC_4096); ms_urldec(enc, el, rel) 248 if ms_contains(rel, ms_slen(rel), ".." as *u8) == 1 { let d: *u8 = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 249 // build full repo-relative path = MS_READER_ROOT + rel (already rooted under reader/ so just strip a leading "reader/") 250 let full: *u8 = sys_mmap(MS_MAGIC_8192); var fo: i64 = 0 251 // rel typically begins "reader/<slug>/..."; map that onto knowledge/staging/media/reader/<slug>/... 252 fo = ms_apps(full, fo, "knowledge/staging/media/" as *u8) 253 fo = ms_apps(full, fo, rel) 254 full[fo] = 0 as u8 255 // enforce the allowlist root prefix 256 if ms_starts(full, fo, MS_READER_ROOT) == 0 { let d: *u8 = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 257 // content type: .json -> application/json; image/* via ms_ctype (jpg/png/gif/webp); else text/plain. 258 // RUNG 3: cbx page<NNN>.jpg / .png must return image/* so the browser renders + zooms them natively. 259 var ct: *u8 = ms_ctype(full) 260 if fo >= 5 { if full[fo-1] == (110 as u8) { if full[fo-2] == (111 as u8) { if full[fo-3] == (115 as u8) { if full[fo-4] == (106 as u8) { ct = "application/json; charset=utf-8" as *u8 } } } } } 261 // ms_ctype returns octet-stream for unknown (e.g. .txt chapters) -> fall back to text/plain for the reader. 262 if ms_starts(ct, ms_slen(ct), "application/octet-stream" as *u8) == 1 { ct = "text/plain; charset=utf-8" as *u8 } 263 let fd: i64 = sys_openat_rd(full) 264 if fd < 0 { let d: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 265 let sz: i64 = sys_lseek(fd, 0, 2) 266 sys_lseek(fd, 0, 0) 267 let hdr: *u8 = sys_mmap(256); var h: i64 = 0 268 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); h = ms_apps(hdr, h, ct) 269 h = ms_apps(hdr, h, "\r\nCache-Control: no-store\r\nContent-Length: " as *u8); h = ms_dec(hdr, h, sz); h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 270 sys_write(cfd, hdr, h) 271 let chunk: *u8 = sys_mmap(MS_MAGIC_262144) 272 var go: i64 = 1 273 while go == 1 { let r: i64 = sys_read(fd, chunk, MS_MAGIC_262144); if r <= 0 { go = 0 } else { sys_write(cfd, chunk, r) } } 274 sys_close(fd); sys_close(cfd) 275 return 0 276} 277 278// SECURITY: a slug must be a single safe path component -- alnum / _ / - ONLY (no "/" or "." -> no traversal, no dotfiles). 279func ms_slug_safe(s: *u8) -> i64 { 280 let n: i64 = ms_slen(s) 281 if n <= 0 { return 0 } 282 if n > 200 { return 0 } 283 var i: i64 = 0 284 while i < n { 285 let c: i64 = s[i] as i64 286 var ok: i64 = 0 287 if c >= 48 { if c <= 57 { ok = 1 } } // 0-9 288 if c >= 65 { if c <= 90 { ok = 1 } } // A-Z 289 if c >= 97 { if c <= 122 { ok = 1 } } // a-z 290 if c == 95 { ok = 1 } // _ 291 if c == 45 { ok = 1 } // - 292 if ok == 0 { return 0 } 293 i = i + 1 294 } 295 return 1 296} 297 298// /api/llmpack?slug=<slug>&budget=<N> -> concat the book's chap<N>.txt, pack into a TOKEN BUDGET (sovereign 299// nx_token_counter, never overflows the window), return an LLM-ready context as a download. The "push a book into 300// an LLM natively" endpoint. Auth-gated (under /api/). Slug strictly validated. Comics (cbx, no chap files) -> 404. 301func ms_handle_llmpack(cfd: i64, p: *u8, plen: i64) -> i64 { 302 let sb: *u8 = sys_mmap(256); ms_qval(p, plen, "slug" as *u8, sb, 256) 303 if ms_slug_safe(sb) == 0 { let d: *u8 = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 304 let bbu: *u8 = sys_mmap(32); ms_qval(p, plen, "budget" as *u8, bbu, 32) 305 var budget: i64 = 0; var bi: i64 = 0 306 while bbu[bi] != (0 as u8) { let c: i64 = bbu[bi] as i64; if c >= 48 { if c <= 57 { budget = budget*10 + (c-48) } } bi = bi + 1 } 307 if budget <= 0 { budget = MS_MAGIC_120000 } 308 if budget > MS_MAGIC_900000 { budget = MS_MAGIC_900000 } 309 // concat chapters reader/<slug>/chap<N>.txt until the first missing one 310 let bigcap: i64 = MS_MAGIC_16777216 311 let big: *u8 = sys_mmap(bigcap); var bn: i64 = 0 312 var ci: i64 = 0; var go: i64 = 1 313 while go == 1 { 314 let path: *u8 = sys_mmap(512); var po: i64 = 0 315 po = ms_apps(path, po, "knowledge/staging/media/reader/" as *u8) 316 po = ms_apps(path, po, sb) 317 po = ms_apps(path, po, "/chap" as *u8) 318 po = ms_dec(path, po, ci) 319 po = ms_apps(path, po, ".txt" as *u8) 320 path[po] = 0 as u8 321 let szp: *i64 = sys_mmap(16) as *i64 322 let cb: *u8 = sys_read_file(path, szp) 323 if (cb as i64) == 0 { go = 0 } else { 324 let cn: i64 = szp[0] 325 if bn + cn + 4 < bigcap { 326 var k: i64 = 0 327 while k < cn { big[bn] = cb[k]; bn = bn + 1; k = k + 1 } 328 big[bn] = 10 as u8; bn = bn + 1; big[bn] = 10 as u8; bn = bn + 1 329 } else { go = 0 } 330 ci = ci + 1 331 if ci > MS_MAGIC_5000 { go = 0 } 332 } 333 } 334 if bn == 0 { let d: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 } 335 let cap: i64 = (budget * 9) / 10 336 let bodycap: i64 = budget * 8 + MS_MAGIC_65536 337 let body: *u8 = sys_mmap(bodycap) 338 let incl: *i64 = sys_mmap(16) as *i64; let omit: *i64 = sys_mmap(16) as *i64 339 pack_text(big, bn, cap, body, bodycap, incl, omit) 340 let blen: i64 = ms_slen(body) 341 let hdr: *u8 = sys_mmap(512); var h: i64 = 0 342 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Disposition: attachment; filename=\"" as *u8) 343 h = ms_apps(hdr, h, sb) 344 h = ms_apps(hdr, h, "-llm.txt\"\r\nCache-Control: no-store\r\nContent-Length: " as *u8) 345 h = ms_dec(hdr, h, blen) 346 h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 347 sys_write(cfd, hdr, h) 348 sys_write(cfd, body, blen) 349 sys_close(cfd) 350 return 0 351} 352 353func ms_serve_browser(cfd: i64) -> i64 { 354 let lenbox: *i64 = sys_mmap(8) as *i64 355 let body: *u8 = sys_read_file(MS_BROWSER, lenbox) 356 let blen: i64 = lenbox[0] 357 let hdr: *u8 = sys_mmap(256); var h: i64 = 0 358 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8) 359 h = ms_dec(hdr, h, blen); h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 360 sys_write(cfd, hdr, h) 361 if blen > 0 { sys_write(cfd, body, blen) } 362 sys_close(cfd) 363 return 0 364} 365 366// /api/remove?p= -> append a SOFT-DELETE PROPOSAL (additive, rule 13: NEVER delete the NAS file) 367func ms_handle_remove(cfd: i64, p: *u8, plen: i64) -> i64 { 368 let enc: *u8 = sys_mmap(MS_MAGIC_8192); let el: i64 = ms_qval(p, plen, "p" as *u8, enc, MS_MAGIC_8192) 369 let path: *u8 = sys_mmap(MS_MAGIC_8192); ms_urldec(enc, el, path) 370 if galx_media_path_ok(path, ms_slen(path)) == 1 { 371 let fd: i64 = sys_openat_append("knowledge/staging/media/removals.tsv" as *u8, 420) 372 if fd >= 0 { sys_write(fd, path, ms_slen(path)); sys_write(fd, "\tproposed-soft-delete\n" as *u8, ms_slen("\tproposed-soft-delete\n" as *u8)); sys_close(fd) } 373 } 374 let r: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 8\r\nConnection: close\r\n\r\n{\"ok\":1}" 375 sys_write(cfd, r, ms_slen(r)); sys_close(cfd) 376 return 0 377} 378 379func ms_hexc(d: i64) -> i64 { if d < 10 { return 48 + d } return 87 + d } 380// deterministic, filesystem-safe slug from a path: "b" + 14 hex of a bounded polynomial hash. 381func ms_pathslug(p: *u8, n: i64, out: *u8) -> i64 { 382 var h: i64 = MS_MAGIC_1469598103 383 var i: i64 = 0 384 while i < n { h = (h * 131 + (p[i] as i64)) % MS_MAGIC_72057594037927931; i = i + 1 } 385 if h < 0 { h = 0 - h } 386 out[0] = 98 as u8 387 let tmp: *u8 = sys_mmap(32) 388 var k: i64 = 0; var hh: i64 = h 389 while k < 14 { tmp[k] = ms_hexc(hh % 16) as u8; hh = hh / 16; k = k + 1 } 390 var j: i64 = 0 391 while j < 14 { out[1 + j] = tmp[13 - j]; j = j + 1 } 392 out[15] = 0 as u8 393 return 15 394} 395// fork+exec an extractor (argv = <path> <slug>), wait, then report 1 if the target json now exists, else 0. 396// Shared by the EPUB/CBX/KINDLE open paths so the kindle chain can try kf8 THEN mobi without duplicating fork glue. 397func ms_run_extractor(extractor: *u8, path: *u8, slug: *u8, target: *u8) -> i64 { 398 let pid: i64 = sys_fork() 399 if pid == 0 { 400 let av: *i64 = sys_mmap(64) as *i64 401 av[0] = extractor as i64 402 av[1] = path as i64 403 av[2] = slug as i64 404 av[3] = 0 405 sys_execve(extractor, av, 0 as *i64) 406 sys_exit(127) 407 } 408 let st: *i64 = sys_mmap(8) as *i64 409 sys_wait4(pid, st, 0) 410 let tfd: i64 = sys_openat_rd(target) 411 if tfd >= 0 { sys_close(tfd); return 1 } 412 return 0 413} 414 415// /api/open?p=<urlenc book path> -> EXTRACT-ON-DEMAND then 302 to the right reader. 416// EPUB -> nx_epub_book -> /reader ; KINDLE (mobi/azw/azw3/prc) -> nx_kf8_book||nx_mobi_book -> /reader ; 417// CBZ/CBR/CB7/PDF -> nx_cbx_pages (magic-sniffing: zip / rar4+rar5 STORE / pdf DCTDecode) -> /cbx. Cached 418// by slug. If extraction yields nothing (DRM, compressed-rar/7z honest-unsupported, vector/text PDF) 419// -> 302 /file (raw download, never a dead end). 420func ms_handle_open(cfd: i64, p: *u8, plen: i64) -> i64 { 421 let raw: *u8 = sys_mmap(MS_MAGIC_8192); let rl: i64 = ms_qval(p, plen, "p" as *u8, raw, MS_MAGIC_8192) 422 let path: *u8 = sys_mmap(MS_MAGIC_8192); let pn: i64 = ms_urldec(raw, rl, path) 423 var dot: i64 = 0 - 1; var x: i64 = 0 424 while x < pn { if path[x] == (46 as u8) { dot = x } x = x + 1 } 425 var kind: i64 = 0 // 1=epub 2=cbz 3=pdf 4=kindle(mobi/azw/azw3/prc) 5=html 6=rtf 0=other 426 if dot >= 0 { 427 let e1: i64 = (path[dot+1] as i64) | 32 428 let e2: i64 = (path[dot+2] as i64) | 32 429 let e3: i64 = (path[dot+3] as i64) | 32 430 if e1 == 101 { if e2 == 112 { if e3 == 117 { kind = 1 } } } // epu(b) 431 if e1 == 99 { if e2 == 98 { if e3 == 122 { kind = 2 } } } // cb(z) 432 if e1 == 99 { if e2 == 98 { if e3 == 114 { kind = 2 } } } // cb(r) -> same extractor: it sniffs MAGIC (zip-lie / rar4 / rar5 STORE) 433 if e1 == 99 { if e2 == 98 { if e3 == 55 { kind = 2 } } } // cb(7) -> sniffed; 7z logs honest-unsupported -> falls to /file 434 if e1 == 112 { if e2 == 100 { if e3 == 102 { kind = 3 } } } // pdf 435 // KINDLE family -> sovereign nx_kf8_book / nx_mobi_book chain (kind 4). .azw and .azw3 share the first 436 // three letters (a,z,w) so one rule covers both; the kf8/mobi fallback sorts MOBI6 vs KF8 at extract time. 437 if e1 == 109 { if e2 == 111 { if e3 == 98 { kind = 4 } } } // mob(i) 438 if e1 == 97 { if e2 == 122 { if e3 == 119 { kind = 4 } } } // azw / azw3 439 if e1 == 112 { if e2 == 114 { if e3 == 99 { kind = 4 } } } // prc 440 if e1 == 104 { if e2 == 116 { if e3 == 109 { kind = 5 } } } // htm(l) -> nx_html_book -> reflow text reader 441 if e1 == 114 { if e2 == 116 { if e3 == 102 { kind = 6 } } } // rtf -> nx_rtf_book -> reflow text reader 442 } 443 let hdr: *u8 = sys_mmap(MS_MAGIC_8192); var h: i64 = 0 444 if kind == 0 { 445 h = ms_apps(hdr, h, "HTTP/1.1 302 Found\r\nLocation: /file?p=" as *u8) 446 var qi2: i64 = 0; while qi2 < rl { hdr[h] = raw[qi2]; h = h + 1; qi2 = qi2 + 1 } 447 h = ms_apps(hdr, h, "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" as *u8) 448 sys_write(cfd, hdr, h); sys_close(cfd); return 0 449 } 450 // R3b: PDF -> sovereign TEXT reflow for mobile. A born-digital PDF with a real text layer is served as 451 // reflowable HTML directly; a scanned/image PDF (little/no text) falls through to the page-image path below. 452 if kind == 3 { 453 if galx_media_path_ok(path, pn) == 1 { 454 let plen2: *i64 = sys_mmap(8) as *i64 455 let pdfb: *u8 = sys_read_file(path, plen2) 456 if (pdfb as i64) != 0 { if plen2[0] > 0 { 457 let rhtml: *u8 = sys_mmap(MS_MAGIC_4194304) 458 let rtxt: *u8 = sys_mmap(MS_MAGIC_4194304) 459 let rdec: *u8 = sys_mmap(MS_MAGIC_4194304) 460 let rtc: *i64 = sys_mmap(8) as *i64 461 let rwlen: i64 = pr_pdf_to_reflow(pdfb, plen2[0], rhtml, rtxt, rdec, rtc) 462 if rtc[0] >= 400 { 463 let rh: *u8 = sys_mmap(256); var rho: i64 = 0 464 rho = ms_apps(rh, rho, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nCache-Control: no-store\r\nContent-Length: " as *u8) 465 rho = ms_dec(rh, rho, rwlen) 466 rho = ms_apps(rh, rho, "\r\nConnection: close\r\n\r\n" as *u8) 467 sys_write(cfd, rh, rho) 468 sys_write(cfd, rhtml, rwlen) 469 sys_close(cfd) 470 return 0 471 } 472 } } 473 } 474 } 475 var extractor: *u8 = "./_offc/nx_epub_book.elf" as *u8 476 var tjson: *u8 = "/book.json" as *u8 477 var route: *u8 = "/library/reader?slug=" as *u8 // /library-prefixed: a redirect Location is resolved by the browser against the origin, NOT <base>, so it must carry the prefix explicitly 478 // CBX (image) route is ONLY comics/scans: cbz (2) + pdf-fallback (3). EPUB (1) + KINDLE (4) are reflowable 479 // text -> the book.json reader route. (Was `kind != 1` which wrongly routed kindle text to the image viewer.) 480 // comics/scans now DEFAULT to the intelligent tiled-zoom reader (/zoom = guided view + per-user resume); 481 // the flat /cbx viewer stays reachable manually at /library/cbx?slug=<slug>. 482 if kind == 2 { extractor = "./_offc/nx_cbx_pages.elf" as *u8; tjson = "/cbx.json" as *u8; route = "/library/zoom?slug=" as *u8 } 483 if kind == 3 { extractor = "./_offc/nx_cbx_pages.elf" as *u8; tjson = "/cbx.json" as *u8; route = "/library/zoom?slug=" as *u8 } 484 if kind == 5 { extractor = "./_offc/nx_html_book.elf" as *u8 } // .html/.htm -> reflow text reader (route+tjson default to reader/book.json) 485 if kind == 6 { extractor = "./_offc/nx_rtf_book.elf" as *u8 } // .rtf -> reflow text reader 486 let slug: *u8 = sys_mmap(32); ms_pathslug(path, pn, slug) 487 let cp: *u8 = sys_mmap(256); var c: i64 = 0 488 c = ms_apps(cp, c, "knowledge/staging/media/reader/" as *u8) 489 var si: i64 = 0; while slug[si] != (0 as u8) { cp[c] = slug[si]; c = c + 1; si = si + 1 } 490 c = ms_apps(cp, c, tjson); cp[c] = 0 as u8 491 // IMAGE-BOOK fast path (kind 1/4): a book already PROMOTED to the zoom viewer carries reader/<slug>/ 492 // cbx.json ("cfix":2). Route straight to zoom -- the text-reader cache dance is for prose. A stale 493 // pre-cfix cbx.json is UNLINKED here so it can never fake a promote downstream. 494 let czp: *u8 = sys_mmap(256); var cz: i64 = 0 495 cz = ms_apps(czp, cz, "knowledge/staging/media/reader/" as *u8) 496 var szi: i64 = 0; while slug[szi] != (0 as u8) { czp[cz] = slug[szi]; cz = cz + 1; szi = szi + 1 } 497 cz = ms_apps(czp, cz, "/cbx.json" as *u8); czp[cz] = 0 as u8 498 var kindtext: i64 = 0 499 if kind == 1 { kindtext = 1 } 500 if kind == 4 { kindtext = 1 } 501 if kindtext == 1 { 502 let zfd: i64 = sys_openat_rd(czp) 503 if zfd >= 0 { 504 let zb: *u8 = sys_mmap(MS_MAGIC_262144); let zn: i64 = sys_read(zfd, zb, MS_MAGIC_262144); sys_close(zfd) 505 var zok: i64 = 0 506 if zn > 0 { if ms_contains(zb, zn, "\"cfix\":2" as *u8) == 1 { zok = 1 } } 507 if zok == 1 { 508 let hz: *u8 = sys_mmap(MS_MAGIC_8192); var ho: i64 = 0 509 ho = ms_apps(hz, ho, "HTTP/1.1 302 Found\r\nLocation: /library/zoom?slug=" as *u8) 510 var qz: i64 = 0; while slug[qz] != (0 as u8) { hz[ho] = slug[qz]; ho = ho + 1; qz = qz + 1 } 511 ho = ms_apps(hz, ho, "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" as *u8) 512 sys_write(cfd, hz, ho); sys_close(cfd); return 0 513 } 514 __syscall(263, 0 - 100, czp, 0, 0, 0, 0) // stale pre-cfix promote -> remove, re-derive below 515 } 516 } 517 var have: i64 = 0 518 let tfd0: i64 = sys_openat_rd(cp) 519 if tfd0 >= 0 { 520 // CACHE-HEAL: a TEXT book (epub=1 / kindle=4) only counts as cached if its book.json carries the CURRENT 521 // reader-fix schema (\"rfix\":1). An older cache (mojibake / "Chapter N" titles / stripped links, extracted 522 // before this fix) is treated as a MISS so it re-extracts with the fixed extractor -- self-healing, no remote 523 // rm. CBX comics (kind 2/3) carry the SAME self-heal via "cfix":2 (natural page order + per-page magic gate 524 // + cbr): an older cbx.json without the current tag re-extracts on next open. (forked-per-request child: 525 // the read buffer dies with the child, no leak.) 526 if kind == 1 { let vb: *u8 = sys_mmap(MS_MAGIC_262144); let vn: i64 = sys_read(tfd0, vb, MS_MAGIC_262144); if vn > 0 { if ms_contains(vb, vn, "\"rfix\":1" as *u8) == 1 { have = 1 } } } 527 else { if kind == 4 { let vb2: *u8 = sys_mmap(MS_MAGIC_262144); let vn2: i64 = sys_read(tfd0, vb2, MS_MAGIC_262144); if vn2 > 0 { if ms_contains(vb2, vn2, "\"rfix\":1" as *u8) == 1 { have = 1 } } } 528 else { if kind == 2 { let vb3: *u8 = sys_mmap(MS_MAGIC_262144); let vn3: i64 = sys_read(tfd0, vb3, MS_MAGIC_262144); if vn3 > 0 { if ms_contains(vb3, vn3, "\"cfix\":2" as *u8) == 1 { have = 1 } } } 529 else { if kind == 3 { let vb4: *u8 = sys_mmap(MS_MAGIC_262144); let vn4: i64 = sys_read(tfd0, vb4, MS_MAGIC_262144); if vn4 > 0 { if ms_contains(vb4, vn4, "\"cfix\":2" as *u8) == 1 { have = 1 } } } 530 else { have = 1 } } } } 531 sys_close(tfd0) 532 } 533 if have == 0 { 534 // RE-EXTRACT NEEDED (missing OR stale pre-rfix cache). Remove any stale book.json FIRST so the kindle 535 // kf8->mobi chain's existence-check is accurate: nx_kf8_book DECLINES a MOBI6 file WITHOUT writing book.json, 536 // and a leftover stale book.json would otherwise fool ms_run_extractor into "kf8 succeeded" -> nx_mobi_book 537 // never runs -> the stale (mojibake) MOBI cache is served forever. (Harmless for epub: it overwrites anyway.) 538 __syscall(263, 0 - 100, cp, 0, 0, 0, 0) // unlinkat(AT_FDCWD, cp) 539 if kind == 4 { 540 // KINDLE: try KF8/AZW3 first (handles dual + pure-KF8 files). nx_kf8_book DECLINES a MOBI6-only file 541 // (returns before writing book.json) so the MOBI6 extractor then catches .mobi/.azw. DRM-protected 542 // files make BOTH decline (no book.json) -> falls through to /file below. No dead end, no guessing. 543 have = ms_run_extractor("./_offc/nx_kf8_book.elf" as *u8, path, slug, cp) 544 if have == 0 { have = ms_run_extractor("./_offc/nx_mobi_book.elf" as *u8, path, slug, cp) } 545 } else { 546 have = ms_run_extractor(extractor, path, slug, cp) 547 } 548 } 549 // IMAGE-BOOK PROMOTE (kind 1/4): text extraction SUCCEEDED, but the "book" may be a COMIC wearing 550 // epub/kindle clothes (~190 full-res page images + ~150-byte anchor "chapters" -- the operator's 551 // "epub type comic books dont load"). The promote organ inspects the extracted dir; image-dominant 552 // -> cbx.json appears -> ZOOM viewer. Real prose exits 2 and stays on the text reader. 553 if kindtext == 1 { if have == 1 { 554 if ms_run_extractor("./_offc/nx_imgbook_promote.elf" as *u8, path, slug, czp) == 1 { route = "/library/zoom?slug=" as *u8 } 555 } } 556 // EPUB DECLINE FALLTHROUGH: an epub whose text extraction declined may still be a ZIP of page images 557 // (fixed-layout comic). The magic-sniffing cbx extractor eats those; cbx.json -> zoom. Never a dead end. 558 if kind == 1 { if have == 0 { 559 have = ms_run_extractor("./_offc/nx_cbx_pages.elf" as *u8, path, slug, czp) 560 if have == 1 { route = "/library/zoom?slug=" as *u8 } 561 } } 562 if have == 1 { 563 h = ms_apps(hdr, h, "HTTP/1.1 302 Found\r\nLocation: " as *u8) 564 h = ms_apps(hdr, h, route) 565 var qi: i64 = 0; while slug[qi] != (0 as u8) { hdr[h] = slug[qi]; h = h + 1; qi = qi + 1 } 566 } else { 567 h = ms_apps(hdr, h, "HTTP/1.1 302 Found\r\nLocation: /file?p=" as *u8) 568 var qj: i64 = 0; while qj < rl { hdr[h] = raw[qj]; h = h + 1; qj = qj + 1 } 569 } 570 h = ms_apps(hdr, h, "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" as *u8) 571 sys_write(cfd, hdr, h) 572 sys_close(cfd) 573 return 0 574} 575 576// /api/cover?p=<urlenc book path> -> serve the book's EMBEDDED cover image (cached under reader/<slug>/, extracted 577// on-demand for epub/mobi/azw). No cover / unsupported -> 404 (the grid shows a text-title card); ".." -> 403. The 578// resolution + slug logic lives in nx_media_cover_lib (shared with the gate). This route is behind THE WALL, so a 579// cover is only served to an authenticated session -- the grid fetches it via fetch()+X-Nishi-Session (no-cookie). 580// ---- cover THUMBNAILS (S-class: never serve a full-res cover into the grid -- the gallery's gs_thumb pattern) ---- 581// REUSE the gallery's proven thumbnailer. Cache-first (a cached thumb serves O(1)); cold -> fork decode->downscale-> 582// encode to .new, atomic rename, serve; FALLBACK to full-res if the generator is absent/fails (zero-regression: 583// without nx_galx_thumb the grid behaves EXACTLY as before). Thumb cached alongside the cover as <cover>.t.jpg. 584const MS_THUMB_ELF: *u8 = "/volume1/ai/galx/nx_galx_thumb.elf" 585const MS_THUMB_MAX: *u8 = "384" 586func ms_fexists(path: *u8) -> i64 { let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 } sys_close(fd); return 1 } 587func ms_thumb_run(src: *u8, outnew: *u8) -> i64 { 588 let argv: *i64 = sys_mmap(48) as *i64 589 argv[0] = MS_THUMB_ELF as i64; argv[1] = src as i64; argv[2] = outnew as i64; argv[3] = MS_THUMB_MAX as i64; argv[4] = 0 590 let envp: *i64 = sys_mmap(8) as *i64; envp[0] = 0 591 let pid: i64 = sys_fork() 592 if pid == 0 { sys_execve(MS_THUMB_ELF, argv, envp); sys_exit(127) } 593 let st: *i64 = sys_mmap(16) as *i64 594 sys_wait4(pid, st, 0) 595 return (st[0] >> 8) & 0xff 596} 597func ms_cover_thumb(cfd: i64, full: *u8) -> i64 { 598 let tpath: *u8 = sys_mmap(640) 599 var o: i64 = ms_apps(tpath, 0, full); o = ms_apps(tpath, o, ".t.jpg" as *u8); tpath[o] = 0 as u8 600 if ms_fexists(tpath) == 1 { return mc_serve_image(cfd, tpath) } // cache hit -> O(1), no re-generation 601 let tnew: *u8 = sys_mmap(640) 602 var o2: i64 = ms_apps(tnew, 0, tpath); o2 = ms_apps(tnew, o2, ".new" as *u8); tnew[o2] = 0 as u8 603 if ms_thumb_run(full, tnew) == 0 { __syscall(82, tnew as i64, tpath as i64, 0, 0, 0, 0) } // atomic rename .new -> cache 604 if ms_fexists(tpath) == 1 { return mc_serve_image(cfd, tpath) } 605 return mc_serve_image(cfd, full) // FALLBACK: generator absent/failed -> full-res (the grid is never broken) 606} 607 608func ms_handle_cover(cfd: i64, p: *u8, plen: i64) -> i64 { 609 let enc: *u8 = sys_mmap(MS_MAGIC_8192); let el: i64 = ms_qval(p, plen, "p" as *u8, enc, MS_MAGIC_8192) 610 let path: *u8 = sys_mmap(MS_MAGIC_8192); let pn: i64 = ms_urldec(enc, el, path) 611 let full: *u8 = sys_mmap(512) 612 let r: i64 = mc_cover_resolve(path, pn, full, 512) 613 if r == 1 { return ms_cover_thumb(cfd, full) } 614 if r < 0 { let d3: *u8 = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d3, ms_slen(d3)); sys_close(cfd); return 0 } 615 let d4: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d4, ms_slen(d4)); sys_close(cfd); return 0 616} 617 618// /api/search?q=<urlenc> -> JSON {results:[{n,p,s}],n} ranked BM25 over the book library (SC20) 619func ms_handle_search(cfd: i64, p: *u8, plen: i64) -> i64 { 620 let raw: *u8 = sys_mmap(MS_MAGIC_1024); let rl: i64 = ms_qval(p, plen, "q" as *u8, raw, MS_MAGIC_1024) 621 let qb: *u8 = sys_mmap(MS_MAGIC_1024); ms_urldec(raw, rl, qb) 622 let tb: *u8 = sys_mmap(16); ms_qval(p, plen, "type" as *u8, tb, 16) 623 if tb[0] == (0 as u8) { tb[0] = 98 as u8; tb[1] = 0 as u8 } // default 'b' (book) 624 let res_off: *i64 = sys_mmap(256) as *i64 625 let res_len: *i64 = sys_mmap(256) as *i64 626 let res_score: *i64 = sys_mmap(256) as *i64 627 let cnt: *i64 = sys_mmap(8) as *i64 628 let bufi: i64 = bks_search(ms_idxpath(tb), qb, res_off, res_len, res_score, 20, cnt) 629 let buf: *u8 = bufi as *u8 630 let out: *u8 = sys_mmap(MS_MAGIC_2000000) 631 var w: i64 = 0 632 w = ms_apps(out, w, "{\"results\":[" as *u8) 633 var i: i64 = 0 634 while i < cnt[0] { 635 let ls: i64 = res_off[i] 636 let ce: i64 = res_off[i] + res_len[i] 637 var bn: i64 = ls 638 var x: i64 = ls 639 while x < ce { if buf[x] == (47 as u8) { bn = x + 1 } x = x + 1 } 640 if i > 0 { w = ms_apps(out, w, "," as *u8) } 641 w = ms_apps(out, w, "{\"n\":\"" as *u8) 642 var y: i64 = bn 643 while y < ce { let ch: i64 = buf[y] as i64; if ch == 34 { w = ms_apps(out, w, "\\\"" as *u8) } else { out[w] = buf[y]; w = w + 1 } y = y + 1 } 644 w = ms_apps(out, w, "\",\"p\":\"" as *u8) 645 y = ls 646 while y < ce { let ch: i64 = buf[y] as i64; if ch == 34 { w = ms_apps(out, w, "\\\"" as *u8) } else { out[w] = buf[y]; w = w + 1 } y = y + 1 } 647 w = ms_apps(out, w, "\",\"s\":" as *u8) 648 w = ms_dec(out, w, res_score[i]) 649 w = ms_apps(out, w, "}" as *u8) 650 i = i + 1 651 } 652 w = ms_apps(out, w, "],\"n\":" as *u8) 653 w = ms_dec(out, w, cnt[0]) 654 w = ms_apps(out, w, "}" as *u8) 655 let hdr: *u8 = sys_mmap(256); var h: i64 = 0 656 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " as *u8) 657 h = ms_dec(hdr, h, w) 658 h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 659 sys_write(cfd, hdr, h) 660 sys_write(cfd, out, w) 661 sys_close(cfd) 662 return 0 663} 664 665// ---- AUTH (S-class Modern Auth, no-cookie; mirrors nx_status_daemon) -------------------------------- 666func ms_body_off(req: *u8, n: i64) -> i64 { 667 var i: i64 = 0 668 while i + 4 <= n { if req[i]==(13 as u8) { if req[i+1]==(10 as u8) { if req[i+2]==(13 as u8) { if req[i+3]==(10 as u8) { return i + 4 } } } } i = i + 1 } 669 return n 670} 671// find "name=" in body; set out_off/out_n to the raw (still %-encoded) value slice. 1/0. 672func ms_form_field(body: *u8, body_n: i64, name: *u8, name_n: i64, out_off: *i64, out_n: *i64) -> i64 { 673 var pos: i64 = 0 674 while pos < body_n { 675 var m: i64 = 1 676 if pos + name_n + 1 > body_n { m = 0 } 677 if m == 1 { var i: i64 = 0; while i < name_n { if (body[pos+i] as i64) != (name[i] as i64) { m = 0; i = name_n } if i < name_n { i = i + 1 } } } 678 if m == 1 { if (body[pos+name_n] as i64) != 61 { m = 0 } } 679 var vend: i64 = pos; var scan: i64 = 1 680 while scan == 1 { if vend >= body_n { scan = 0 } if scan == 1 { if (body[vend] as i64) == 38 { scan = 0 } } if scan == 1 { vend = vend + 1 } } 681 if m == 1 { out_off[0] = pos + name_n + 1; out_n[0] = vend - (pos + name_n + 1); return 1 } 682 pos = vend + 1 683 } 684 return 0 685} 686func ms_emit_401(cfd: i64) -> i64 { 687 let d: *u8 = "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 24\r\n\r\n{\"error\":\"unauthorized\"}" as *u8 688 sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 689} 690// POST /api/login: handle=<h>&passphrase=<p> (form-urlencoded) -> nx_modern_auth_login -> 200 {token} | 401 691func ms_login(ctx: *NxAuthContext, cfd: i64, req: *u8, req_n: i64) -> i64 { 692 let body_off: i64 = ms_body_off(req, req_n) 693 let body: *u8 = ((req as i64) + body_off) as *u8 694 let body_n: i64 = req_n - body_off 695 let hoff: *i64 = sys_mmap(8) as *i64; let hnn: *i64 = sys_mmap(8) as *i64 696 let poff2: *i64 = sys_mmap(8) as *i64; let pnn: *i64 = sys_mmap(8) as *i64 697 let out: *u8 = sys_mmap(MS_MAGIC_4096); var o: i64 = 0; var ok: i64 = 0 698 if ms_form_field(body, body_n, "handle" as *u8, 6, hoff, hnn) == 1 { 699 if ms_form_field(body, body_n, "passphrase" as *u8, 10, poff2, pnn) == 1 { 700 let hbuf: *u8 = sys_mmap(256); let pbuf: *u8 = sys_mmap(512) 701 let h_dec: i64 = ms_urldec(((body as i64)+hoff[0]) as *u8, hnn[0], hbuf) 702 let p_dec: i64 = ms_urldec(((body as i64)+poff2[0]) as *u8, pnn[0], pbuf) 703 if h_dec > 0 { if p_dec > 0 { 704 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES) 705 let tok_n: *i64 = sys_mmap(8) as *i64; tok_n[0] = 0 706 if nx_modern_auth_login(ctx, hbuf, h_dec, pbuf, p_dec, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) == NX_MAUTH_OK { 707 let b64: *u8 = sys_mmap(256) 708 let b64_n: i64 = b64_encode(tok, NX_MAUTH_SESSION_TOKEN_BYTES, b64) 709 o = ms_apps(out, o, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8) 710 o = ms_dec(out, o, 12 + b64_n) 711 o = ms_apps(out, o, "\r\n\r\n{\"token\":\"" as *u8) 712 var z: i64 = 0; while z < b64_n { out[o]=b64[z]; o=o+1; z=z+1 } 713 o = ms_apps(out, o, "\"}" as *u8) 714 ok = 1 715 } 716 } } 717 } 718 } 719 if ok == 0 { o = ms_apps(out, 0, "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 24\r\n\r\n{\"error\":\"unauthorized\"}" as *u8) } 720 sys_write(cfd, out, o); sys_close(cfd) 721 return 0 722} 723// the no-cookie SPA shell: login form -> token in sessionStorage -> all calls carry X-Nishi-Session 724func ms_shell(cfd: i64) -> i64 { 725 // body FIRST so Content-Length is exact. PROTOCOL FIX (the /library "10s stall"): this was the ONE 726 // response in the daemon without Content-Length -- EOF-delimited consumers (the edge buffered relay, 727 // and every client behind it) had to wait ~10s for an idle-close to know the body ended. With the 728 // exact length, clients complete immediately. Same pattern as every other handler in this file. 729 let out: *u8 = sys_mmap(MS_MAGIC_32768); var o: i64 = 0 730 o = ms_apps(out, o, "<!DOCTYPE html><!--nrbuild=7 +content-length-exact +session-ttl-24h+no-logout-on-transient+back-history--><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><base href=\"/library/\"><title>Nishi Library</title>" as *u8) 731 o = ms_apps(out, o, "<style>body{font:15px system-ui,-apple-system,Segoe UI,sans-serif;margin:0;background:#0b1220;color:#e6edf6}#lg{max-width:330px;margin:12vh auto;padding:0 20px}#lg input{width:100%;padding:10px;margin:.4rem 0;box-sizing:border-box;border:1px solid #28406a;border-radius:7px;background:#0e1a30;color:#e6edf6}button{padding:10px 18px;border:0;border-radius:7px;background:#e8a534;color:#0b2545;font-weight:700}.e{color:#ff8a8a;min-height:1.2em}#hd{background:#0b2545;padding:10px 14px;display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center;position:sticky;top:0;z-index:5}#hd b{font-size:1.05rem;white-space:nowrap}#q{flex:1 1 200px;min-width:0;padding:9px 12px;border-radius:8px;border:1px solid #28406a;background:#0e1a30;color:#e6edf6;font-size:1rem}#gr{display:grid;grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:12px;padding:14px}.c{background:#13233f;border-radius:10px;overflow:hidden;cursor:pointer}.c img.cov{width:100%;aspect-ratio:2/3;object-fit:cover;display:none;background:#0e1a30}.c .t{padding:8px 9px;font-size:.74rem;line-height:1.3;word-break:break-word}@media(max-width:480px){#hd b{width:100%}#gr{grid-template-columns:repeat(auto-fill,minmax(102px,1fr));gap:9px;padding:10px}}</style>" as *u8) 732 o = ms_apps(out, o, "<div id=lg><h2>&#128218; Nishi Library</h2><input id=h placeholder=handle autocomplete=username autofocus><input id=p type=password placeholder=passphrase autocomplete=current-password><button id=b>Sign in</button><p id=e class=e></p></div><div id=app hidden><div id=hd><b>&#128218; Library</b><input id=q placeholder=\"search…\"><a href=\"manga\" style=\"padding:9px 14px;border-radius:8px;background:#33507f;color:#e6edf6;text-decoration:none;font-weight:700;white-space:nowrap\">&#128218;+ My Manga</a><a href=\"shelf\" style=\"padding:9px 14px;border-radius:8px;background:#33507f;color:#e6edf6;text-decoration:none;font-weight:700;white-space:nowrap\">&#128214; Comics</a><a href=\"health\" style=\"padding:9px 14px;border-radius:8px;background:#294066;color:#e6edf6;text-decoration:none;font-weight:700;white-space:nowrap\">&#9877; Health</a></div><div id=gr></div></div>" as *u8) 733 o = ms_apps(out, o, "<script>function T(){return sessionStorage.nx_sess||''}function H(){return {'X-Nishi-Session':T()}}function app(){document.getElementById('lg').hidden=1;document.getElementById('app').hidden=0;list('')}var IO=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){IO.unobserve(e.target);lc(e.target)}})},{rootMargin:'500px'});function lc(d){fetch('api/cover?p='+encodeURIComponent(d.dataset.p),{headers:H()}).then(function(r){if(!r.ok)throw 0;return r.blob()}).then(function(b){var im=d.firstChild;im.src=URL.createObjectURL(b);im.style.display='block'}).catch(function(){})}" as *u8) 734 o = ms_apps(out, o, "var OFF=0,TOTAL=-1,BUSY=0,MODE='browse';function card(it){var d=document.createElement('div');d.className='c';d.dataset.p=it.p;var im=document.createElement('img');im.className='cov';im.alt='';var t=document.createElement('div');t.className='t';t.textContent=it.n;d.appendChild(im);d.appendChild(t);d.onclick=function(){openb(it.p)};IO.observe(d);return d}function lerr(q){var g=document.getElementById('gr');g.innerHTML='';var p=document.createElement('p');p.style.cssText='padding:16px;color:#9ab';p.textContent='Couldn\\u2019t reach the library \\u2014 tap to retry.';p.onclick=function(){list(q)};g.appendChild(p)}function list(q){var g=document.getElementById('gr');g.innerHTML='';OFF=0;TOTAL=-1;BUSY=0;if(q){MODE='search';fetch('api/search?type=book&q='+encodeURIComponent(q),{headers:H()}).then(function(r){if(r.status===401||r.status===403){sessionStorage.removeItem('nx_sess');location.reload();return null}if(!r.ok)throw 0;return r.json()}).then(function(j){if(!j)return;(j.results||[]).forEach(function(it){g.appendChild(card(it))})}).catch(function(){lerr(q)})}else{MODE='browse';loadMore()}}" as *u8) 735 o = ms_apps(out, o, "function loadMore(){if(BUSY||MODE!=='browse')return;if(TOTAL>=0&&OFF>=TOTAL)return;BUSY=1;fetch('api/list?type=book&offset='+OFF,{headers:H()}).then(function(r){if(r.status===401||r.status===403){sessionStorage.removeItem('nx_sess');location.reload();return null}if(!r.ok)throw 0;return r.json()}).then(function(j){if(!j){BUSY=0;return}TOTAL=j.total||0;var a=j.items||[];var g=document.getElementById('gr');a.forEach(function(it){g.appendChild(card(it))});OFF=OFF+a.length;BUSY=0;if(a.length===0){TOTAL=OFF;return}if(OFF<TOTAL&&window.innerHeight>=document.body.offsetHeight-120)loadMore()}).catch(function(){BUSY=0;if(OFF===0)lerr('')})}window.addEventListener('scroll',function(){if(window.innerHeight+window.scrollY>=document.body.offsetHeight-900)loadMore()},{passive:true});" as *u8) 736 o = ms_apps(out, o, "function openb(p){fetch('api/open?p='+encodeURIComponent(p),{cache:'no-store',headers:H()}).then(function(r){var u=r.url||'';if(u.indexOf('/reader')>=0||u.indexOf('/cbx')>=0){location.href=u;return null}if(u.indexOf('/file')>=0){alert('This book can\\u2019t be opened in the reader \\u2014 it may be DRM-protected or an unsupported format.');return null}return r.text()}).then(function(t){if(t){document.open();document.write(t);document.close()}})}" as *u8) 737 o = ms_apps(out, o, "document.getElementById('b').onclick=function(){var b='handle='+encodeURIComponent(document.getElementById('h').value)+'&passphrase='+encodeURIComponent(document.getElementById('p').value);fetch('api/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b}).then(function(r){if(!r.ok)throw 0;return r.json()}).then(function(j){sessionStorage.nx_sess=j.token;app()}).catch(function(){document.getElementById('e').textContent='Wrong handle or passphrase.'})};var qi=document.getElementById('q');qi.oninput=function(){list(qi.value.trim())};if(sessionStorage.nx_sess){app()}</script>" as *u8) 738 let hdr: *u8 = sys_mmap(256); var h: i64 = 0 739 h = ms_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8) 740 h = ms_dec(hdr, h, o) 741 h = ms_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8) 742 sys_write(cfd, hdr, h); sys_write(cfd, out, o); sys_close(cfd) 743 return 0 744} 745 746func ms_emit_403(cfd: i64) -> i64 { 747 let d: *u8 = "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nContent-Length: 16\r\nConnection: close\r\n\r\n{\"error\":\"nope\"}" 748 sys_write(cfd, d, ms_slen(d)); sys_close(cfd); return 0 749} 750// ISOLATION GUARD: extract the request's book slug and, if it is a PERSONAL slug ("u_.."), require the caller owns 751// it. The slug is carried either as `slug=<slug>` (zoomprep/pos/ann) or inside `p=reader/<slug>/..` (api/book, the 752// %2F-or-/ separator). Shared slugs + requests with no slug -> allowed (return 1). Uses rl_slug_guard. 753func ms_slug_guarded(ukey: *u8, path: *u8, plen: i64) -> i64 { 754 let slug: *u8 = sys_mmap(256) 755 var have: i64 = 0 756 // try slug=<v> 757 let sv: i64 = ms_qval(path, plen, "slug" as *u8, slug, 256) 758 if sv > 0 { have = 1 } else { 759 // try p=reader/<slug>/.. (p is urlencoded; the leading "reader/" may be literal or %2F-escaped) 760 let enc: *u8 = sys_mmap(MS_MAGIC_4096); let el: i64 = ms_qval(path, plen, "p" as *u8, enc, MS_MAGIC_4096) 761 if el > 0 { 762 let dec: *u8 = sys_mmap(MS_MAGIC_4096); ms_urldec(enc, el, dec) 763 let dn: i64 = ms_slen(dec) 764 if ms_starts(dec, dn, "reader/" as *u8) == 1 { 765 var i: i64 = 7; var o: i64 = 0 766 while i < dn { if dec[i] == (47 as u8) { i = dn } else { if o < 200 { slug[o] = dec[i]; o = o + 1 } i = i + 1 } } 767 slug[o] = 0 as u8 768 if o > 0 { have = 1 } 769 } 770 } 771 } 772 if have == 0 { return 1 } // no book slug in this request -> not our concern 773 return rl_slug_guard(ukey, slug) // shared -> 1 ; own personal -> 1 ; other's personal -> 0 774} 775 776func ms_handle(ctx: *NxAuthContext, cfd: i64) -> i64 { 777 let req: *u8 = sys_mmap(MS_MAGIC_65536) 778 let m: *i64 = sys_mmap(8) as *i64; let po: *i64 = sys_mmap(8) as *i64; let pl: *i64 = sys_mmap(8) as *i64 779 let cl: *i64 = sys_mmap(8) as *i64; let bo: *i64 = sys_mmap(8) as *i64; let rn: *i64 = sys_mmap(8) as *i64 780 let rc: i64 = nx_http_server_read_request(cfd, req, MS_MAGIC_65536, m, po, pl, cl, bo, rn) 781 if rc != 0 { sys_close(cfd); return 0 } 782 var path: *u8 = ((req as i64) + po[0]) as *u8 783 var plen: i64 = pl[0] 784 // /library prefix-awareness: this reader is reverse-proxied at nishifamily.com/library (sd2_proxy_to 785 // forwards the path UNCHANGED) and the served HTML carries <base href="/library/">, so the browser always 786 // requests /library/*. Strip a leading "/library" here so every route below matches -- and bare :8791 787 // access works identically (the <base> makes even direct hits go through /library). 8 = len("/library"). 788 if ms_starts(path, plen, "/library" as *u8) == 1 { path = ((path as i64) + 8) as *u8; plen = plen - 8; if plen <= 0 { path = "/" as *u8; plen = 1 } } 789 // nishifamily.com/media (the family-facing SFW library name) is reverse-proxied here too -- strip /media the 790 // same way so every route below matches. 6 = len("/media"). (operator 2026-06-18: /media live for everyone) 791 if ms_starts(path, plen, "/media" as *u8) == 1 { path = ((path as i64) + 6) as *u8; plen = plen - 6; if plen <= 0 { path = "/" as *u8; plen = 1 } } 792 // login is the only unauthenticated endpoint 793 if ms_starts(path, plen, "/api/login" as *u8) == 1 { return ms_login(ctx, cfd, req, rn[0]) } 794 // UI SHELLS served ungated -- they hold NO book data; the data they fetch (/api/book, /file) is the gated 795 // part and carries the sessionStorage token. So a real navigation to /reader?slug / /cbx?slug works (the 796 // shell renders, then its own fetches present the token). No-cookie cardinal intact. 797 if ms_starts(path, plen, "/reader" as *u8) == 1 { return ms_serve_static(cfd, MS_READER, "text/html; charset=utf-8" as *u8) } 798 if ms_starts(path, plen, "/cbx" as *u8) == 1 { return ms_serve_static(cfd, MS_CBX, "text/html; charset=utf-8" as *u8) } 799 if ms_starts(path, plen, "/zoom" as *u8) == 1 { return ms_serve_static(cfd, MS_ZOOM, "text/html; charset=utf-8" as *u8) } 800 if ms_starts(path, plen, "/manga" as *u8) == 1 { return ms_serve_static(cfd, MS_MANGA, "text/html; charset=utf-8" as *u8) } 801 if ms_starts(path, plen, "/shelf" as *u8) == 1 { return ms_serve_static(cfd, MS_SHELF, "text/html; charset=utf-8" as *u8) } // LOCAL comics/manga series browse shell (data via /api/shelf, behind the wall) 802 if ms_starts(path, plen, "/health" as *u8) == 1 { return ms_serve_static(cfd, MS_HEALTH, "text/html; charset=utf-8" as *u8) } // media-liveness dashboard shell (data via /api/mediahealth, behind the wall) 803 // COVER PROXY: served ungated because <img src> cannot carry the session header; guarded by mss_handle_thumb's 804 // host allow-list (only mangadex/nhentai cover CDNs -> public thumbnails, no user data, not an open proxy). 805 if ms_starts(path, plen, "/api/thumb" as *u8) == 1 { return mss_handle_thumb(cfd, path, plen) } 806 // THE WALL: every DATA route requires a valid no-cookie X-Nishi-Session token. The handle-returning variant 807 // (same validation path) also yields the authenticated USER -> ukey keys the per-user sync spine (R1). 808 let uhb: *u8 = sys_mmap(96) 809 let uhn: *i64 = sys_mmap(8) as *i64 810 if nx_sa_validate_handle(ctx, req, rn[0], sys_now_realtime_sec(), uhb, 64, uhn) != NX_MAUTH_OK { 811 if ms_starts(path, plen, "/api/" as *u8) == 1 { return ms_emit_401(cfd) } 812 if ms_starts(path, plen, "/file" as *u8) == 1 { return ms_emit_401(cfd) } 813 return ms_shell(cfd) 814 } 815 let ukey: *u8 = sys_mmap(64) 816 sy_user_key(uhb, uhn[0], ukey) 817 // ---- shared/personal library routes (owner-in-slug isolation) ---- 818 // POST /api/manga/add {url,title} -> download into THIS user's private area 819 if ms_starts(path, plen, "/api/manga/add" as *u8) == 1 { 820 if req[0] == (80 as u8) { 821 var mbl: i64 = cl[0] 822 if bo[0] + mbl > rn[0] { mbl = rn[0] - bo[0] } 823 return ma_handle_add(cfd, req, bo[0], mbl, ukey) 824 } 825 return ms_emit_401(cfd) 826 } 827 // ---- SOURCE BROWSER (behind the wall): search external sources, list chapters, proxy covers ---- 828 // GET /api/msearch?src=<mangadex|nhentai>&q=<query> -> unified results JSON 829 if ms_starts(path, plen, "/api/msearch" as *u8) == 1 { return mss_handle_search(cfd, path, plen) } 830 // GET /api/mbrowse?src=&mode=<popular|latest> -> the LANDING FEED (browse without typing) 831 if ms_starts(path, plen, "/api/mbrowse" as *u8) == 1 { return mss_handle_browse(cfd, path, plen) } 832 // GET /api/mrandom?src= -> one random title 833 if ms_starts(path, plen, "/api/mrandom" as *u8) == 1 { return mss_handle_random(cfd, path, plen) } 834 // GET /api/mediahealth -> the media-liveness sweep report (written by nx_movie_probe --sweep). Behind the wall. 835 if ms_starts(path, plen, "/api/mediahealth" as *u8) == 1 { return ms_serve_static(cfd, MS_HEALTH_JSON, "application/json" as *u8) } 836 // GET /api/dlstatus?slug= -> live download progress {done,total,finished} for THIS user's in-flight book 837 if ms_starts(path, plen, "/api/dlstatus" as *u8) == 1 { return ma_handle_dlstatus(cfd, path, plen, ukey) } 838 // GET /api/mfeed?id=<mangaId> -> chapter list (mangadex series -> chapters) 839 if ms_starts(path, plen, "/api/mfeed" as *u8) == 1 { return mss_handle_feed(cfd, path, plen) } 840 // POST /api/import {url,title} -> POLITELY import images (honors robots.txt) into THIS user's private area 841 if ms_starts(path, plen, "/api/import" as *u8) == 1 { 842 if req[0] == (80 as u8) { 843 var ibl: i64 = cl[0] 844 if bo[0] + ibl > rn[0] { ibl = rn[0] - bo[0] } 845 return ma_handle_import(cfd, req, bo[0], ibl, ukey) 846 } 847 return ms_emit_401(cfd) 848 } 849 // GET /api/personal -> the caller's OWN private books (isolation by ukey) 850 if ms_starts(path, plen, "/api/personal" as *u8) == 1 { 851 let pb: *u8 = sys_mmap(MS_MAGIC_65536) 852 rl_list_personal_json(ukey, pb, MS_MAGIC_65536) 853 return zs_emit(cfd, "200 OK" as *u8, pb) 854 } 855 // POST /api/promote?slug=<u_..slug> -> move the caller's personal book into the shared family library 856 if ms_starts(path, plen, "/api/promote" as *u8) == 1 { 857 let sb: *u8 = sys_mmap(256); ms_qval(path, plen, "slug" as *u8, sb, 256) 858 let pr: i64 = rl_promote_to_shared(ukey, sb) 859 if pr == 1 { return zs_emit(cfd, "200 OK" as *u8, "{\"ok\":1,\"area\":\"shared\"}" as *u8) } 860 return zs_emit(cfd, "200 OK" as *u8, "{\"ok\":0}" as *u8) 861 } 862 // ★ ISOLATION GUARD: any by-slug route that names a PERSONAL slug (u_<ukey>_..) must be the caller's own. 863 // Shared slugs + no-slug requests pass through. Fail-closed 403 otherwise -- before open/book/zoomprep/pos/ann. 864 if ms_slug_guarded(ukey, path, plen) == 0 { return ms_emit_403(cfd) } 865 // GET /api/shelf -> the LOCAL series->volumes catalog (idx scan + data-driven grouping; nx_manga_shelf_lib). 866 // Forked child, fresh from disk truth every request -- no cache to go stale. 867 if ms_starts(path, plen, "/api/shelf" as *u8) == 1 { 868 let shb: *u8 = sys_mmap(MS_MAGIC_2097152) 869 msl_shelf_json(shb, MS_MAGIC_2097152) 870 return zs_emit(cfd, "200 OK" as *u8, shb) 871 } 872 if ms_starts(path, plen, "/api/open" as *u8) == 1 { return ms_handle_open(cfd, path, plen) } 873 if ms_starts(path, plen, "/api/cover" as *u8) == 1 { return ms_handle_cover(cfd, path, plen) } 874 if ms_starts(path, plen, "/api/search" as *u8) == 1 { return ms_handle_search(cfd, path, plen) } 875 if ms_starts(path, plen, "/api/list" as *u8) == 1 { return ms_handle_list(cfd, path, plen) } 876 if ms_starts(path, plen, "/api/remove" as *u8) == 1 { return ms_handle_remove(cfd, path, plen) } 877 if ms_starts(path, plen, "/api/book" as *u8) == 1 { sy_record_auto(path, plen, ukey, sys_now_realtime_sec()); return ms_handle_book(cfd, path, plen) } 878 if ms_starts(path, plen, "/api/pos" as *u8) == 1 { 879 if req[0] == (80 as u8) { // 'P' = POST -> store; else GET -> read 880 var sbl: i64 = cl[0] 881 if bo[0] + sbl > rn[0] { sbl = rn[0] - bo[0] } 882 return sy_handle_pos_put(cfd, path, plen, req, bo[0], sbl, ukey) 883 } 884 return sy_handle_pos_get(cfd, path, plen, ukey) 885 } 886 if ms_starts(path, plen, "/api/ann" as *u8) == 1 { 887 if req[0] == (80 as u8) { 888 var abl: i64 = cl[0] 889 if bo[0] + abl > rn[0] { abl = rn[0] - bo[0] } 890 return sy_handle_ann_put(cfd, path, plen, req, bo[0], abl, ukey) 891 } 892 return sy_handle_ann_get(cfd, path, plen, ukey) 893 } 894 if ms_starts(path, plen, "/api/zoomprep" as *u8) == 1 { return zs_handle_zoomprep(cfd, path, plen) } 895 if ms_starts(path, plen, "/api/llmpack" as *u8) == 1 { return ms_handle_llmpack(cfd, path, plen) } 896 if ms_starts(path, plen, "/cbx" as *u8) == 1 { return ms_serve_static(cfd, MS_CBX, "text/html; charset=utf-8" as *u8) } 897 if ms_starts(path, plen, "/reader" as *u8) == 1 { return ms_serve_static(cfd, MS_READER, "text/html; charset=utf-8" as *u8) } 898 if ms_starts(path, plen, "/file" as *u8) == 1 { return ms_handle_file(cfd, path, plen, req, rn[0]) } 899 return ms_shell(cfd) 900} 901 902func main(argc: i64, argv: *i64) -> i64 { 903 // S-class auth wall: args [1]=keysfile [2]=storefile [3]=realm. Reuse the "Nishi Wiki" realm + keys so the 904 // operator's EXISTING login works. Fail-fast (Rule 20): arm the realm context at startup or refuse to serve. 905 if argc < 4 { ms_puts("usage: nx_media_server <keysfile> <storefile> <realm>\n" as *u8); return 1 } 906 let keysfile: *u8 = argv[1] as *u8 907 let storefile: *u8 = argv[2] as *u8 908 let realm: *u8 = argv[3] as *u8 909 let realm_n: i64 = ms_slen(realm) 910 let oprf_seed: *u8 = sys_mmap(32) 911 let akp: *u8 = sys_mmap(32) 912 let akb: *u8 = sys_mmap(33) 913 let edp: *u8 = sys_mmap(32) 914 let edb: *u8 = sys_mmap(32) 915 if nx_uas_server_keys_load_or_init(keysfile, oprf_seed, akp, akb, edp, edb) != NX_UAS_OK { ms_puts("FATAL: server-key bundle load\n" as *u8); return 2 } 916 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 917 // session TTL: 86400s = 24h = NX_MAUTH_HARD_MAX_TTL_S (the auth layer's HARD CAP -- a larger value makes 918 // nx_auth_context_init return BAD_INPUT and the server crash-loops). Was 900s=15min, which logged you out 919 // mid-read; 24h is the max allowed and the token lives in tab-scoped sessionStorage (cleared on tab close). 920 if nx_auth_context_init(ctx, realm, realm_n, realm, realm_n, storefile as i64, oprf_seed, edp, edb, MS_MAGIC_86400, MS_MAGIC_8192, 1, 1, 5, 1) != NX_MAUTH_OK { ms_puts("FATAL: auth context init\n" as *u8); return 3 } 921 var port: i64 = MS_PORT 922 if argc >= 5 { var pv: i64 = 0; var pi: i64 = 0; let ps: *u8 = argv[4] as *u8; while ps[pi] != (0 as u8) { if ps[pi] >= (48 as u8) { if ps[pi] <= (57 as u8) { pv = pv*10 + ((ps[pi] as i64) - 48) } } pi = pi + 1 } if pv > 0 { port = pv } } 923 let addr: *u8 = sys_mmap(16) 924 // LAN bind (0.0.0.0); every request now passes THE WALL (nx_sa_validate, no-cookie X-Nishi-Session) before 925 // any catalog/book bytes are served. PUBLIC-internet exposure still deferred to front-end routing. 926 nx_http_server_addr_any(addr, port) 927 let lv: *i64 = sys_mmap(8) as *i64 928 let lfd: i64 = nx_http_server_listen(addr, 64, lv) 929 if lfd < 0 { ms_puts("media-server listen FAILED\n" as *u8); return 1 } 930 ms_puts("nx_media_server SOVEREIGN on http://0.0.0.0:8790 (LAN / internal-network) -- no python\n" as *u8) 931 let av: *i64 = sys_mmap(8) as *i64 932 let st: *i64 = sys_mmap(8) as *i64 933 var go: i64 = 1 934 while go == 1 { 935 // Reap ALL exited children each pass (not just one) so per-request fork children can't pile up as 936 // zombies toward RLIMIT_NPROC and starve fork() -> the parent would then close() every new connection 937 // unhandled = /library serving one request then -4 until killed. Mirrors the supervisor's own reaper. 938 var rr: i64 = sys_wait4(0 - 1, st, 1) 939 while rr > 0 { rr = sys_wait4(0 - 1, st, 1) } 940 let cfd: i64 = nx_http_server_accept_one(lfd, av) 941 if cfd >= 0 { 942 let pid: i64 = sys_fork() 943 if pid == 0 { ms_handle(ctx, cfd); sys_exit(0) } 944 sys_close(cfd) 945 } 946 } 947 return 0 948}