code wiki / (root) / nx_vault_capture.nx

nx_vault_capture.nx source

↩ module page · 1074 lines · 42799 B

1// nx_vault_capture.nx -- SAVE, as a first-class sovereign verb. (v2: ref-root/phys-root split) 2// 3// OPERATOR 2026-08-06: "saving an image doesn't just go to a download folder -- 4// you have the ability to save the media or the whole site for future 5// consumption if you choose, from the browser to the OS." 6// 7// WHAT THIS DOES: one verb, three scopes, one destination. 8// media <url> the asset alone 9// page <url> the document AS AN ITEM + every image it references 10// site <url> a bounded same-host crawl, every page captured as above 11// Each captured byte becomes a content-addressed vault item (CID = identity, so 12// re-saving is free and duplicates collapse) carrying its provenance at ingest. 13// 14// ONE CHOKEPOINT (vc_ingest_file): the album downloader (mvf_fetch_album_pfx) 15// filed downloads into collections but wrote NO item record, and does not even 16// import the ingest module -- so it structurally COULD NOT. A capture path that 17// does its own recording can forget to record. So there is exactly one way in, 18// and the gate asserts the invariant it holds: EVERY COLLECTION MEMBER RESOLVES 19// TO A RECORD (T6), with a negative control (T7). 20// 21// REF-ROOT vs PHYS-ROOT (v2, the fix for the v1 15/15 live failure). The 22// record's `ref` uses the LOGICAL DSM root /volume1/vault -- a shared folder 23// that needs root/DSM to create and DOES NOT EXIST as a plain dir. All existing 24// records carry that logical root, and nx_vault_gateway SERVES by stripping its 25// 14-char prefix and prepending a writable PHYSICAL root (~/vaultfs). v1 used 26// ONE root for both the ref and the bytes, so every write under the nonexistent 27// /volume1/vault failed (a page + 14 assets = 15/15). v2 keeps the LOGICAL ref 28// in the record (consistent with existing records + the gateway remap) and 29// writes bytes to the PHYSICAL root. Both roots are data-driven 30// (knowledge/status/vault_roots.conf), rule 11/17. 31// 32// ORDERING (nx_mvault_coll): collections are declared BEFORE the bytes land. 33// Composes only proven organs. license_tier: ORIGINAL 34 35import "nx_syscalls.nx" 36import "nx_canon_cid.nx" 37import "nx_registry.nx" 38import "nx_media_pool.nx" 39import "nx_media_sniff.nx" 40import "nx_mvault_ingest.nx" 41import "nx_mvault_context.nx" 42import "nx_mvault_record.nx" 43import "nx_mvault_layout.nx" 44import "nx_mvault_reclaim.nx" 45import "nx_mvault_reindex.nx" 46import "nx_mvault_tag.nx" 47import "nx_mvault_coll.nx" 48import "nx_mvault_fetch.nx" 49import "nx_https_fetch_lib.nx" 50import "nx_paced_fetch.nx" // R0 (/compare/mediaingest): the ONE pacing hook around every capture fetch -- see vc_pace_hook 51import "nx_ingest_admit.nx" 52import "nx_url_safety_lib.nx" // R11: the ONE SSRF url ruler, shared with nx_clean_serve_daemon and nx_cleanview // R2 (/compare/mediaingest): disk-floor admission at the chokepoint -- refuse a torn write on a full volume 53const VC_MAGIC_4096: i64 = 4096 54const VC_DISK_REFUSE: i64 = 0 - 2 // vc_ingest_file's distinct return for a disk-floor refusal (callers read <0 as fail, correctly) 55const VC_EXIT_UNSAFE_URL: i64 = 9 // R11: the edge verb refused an SSRF-unsafe url; distinct from a fetch failure (6) so a caller can tell a REFUSAL from a FAILURE 56const VC_MAGIC_1024: i64 = 1024 57const VC_MAGIC_65536: i64 = 65536 58const VC_MAGIC_8192: i64 = 8192 59 60const VC_URL_CAP: i64 = 2048 61const VC_PATH_CAP: i64 = 2048 62const VC_PAGE_CAP: i64 = 4194304 63const VC_LINKS_CAP: i64 = 1048576 64const VC_ITEMS_CAP: i64 = 262144 65const VC_VISIT_CAP: i64 = 262144 66const VC_HEAD_CAP: i64 = 8192 67const VC_REC_CAP: i64 = 4096 68const VC_REPORT_CAP: i64 = 65536 69const VC_EXT_CAP: i64 = 64 70const VC_HOST_CAP: i64 = 512 71const VC_CID_CAP: i64 = 128 72const VC_ROOT_CAP: i64 = 512 73 74const VC_DEF_ASSETS: i64 = 64 75const VC_DEF_PAGES: i64 = 16 76const VC_MAX_ASSETS: i64 = 512 77const VC_MAX_PAGES: i64 = 256 78 79// Compiled defaults; overridable from knowledge/status/vault_roots.conf. 80const VC_REF_ROOT: *u8 = "/volume1/vault" as *u8 81const VC_PHYS_ROOT: *u8 = "/volume1/homes/elderwesto/vaultfs" as *u8 82const VC_ROOTS_CONF: *u8 = "knowledge/status/vault_roots.conf" as *u8 83 84const VC_ST_FOUND: i64 = 0 85const VC_ST_NEW: i64 = 1 86const VC_ST_DUP: i64 = 2 87const VC_ST_FAIL: i64 = 3 88const VC_ST_PAGE: i64 = 4 89const VC_ST_PAGES: i64 = 5 90const VC_ST_N: i64 = 6 91 92func vc_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 93 94func vc_cat(dst: *u8, off: i64, s: *u8) -> i64 { 95 var i: i64 = 0 96 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 } 97 return off + i 98} 99 100func vc_eq(a: *u8, b: *u8) -> i64 { 101 var i: i64 = 0 102 while 1 == 1 { 103 if a[i] != b[i] { return 0 } 104 if a[i] == (0 as u8) { return 1 } 105 i = i + 1 106 } 107 return 0 108} 109 110func vc_w(s: *u8) -> i64 { sys_write(1, s, vc_strlen(s)); return 0 } 111func vc_werr(s: *u8) -> i64 { sys_write(2, s, vc_strlen(s)); return 0 } 112 113func vc_n(v: i64) -> i64 { 114 let d: *u8 = sys_mmap(32) 115 let dl: i64 = mv_u_dec(d, 0, v) 116 sys_write(1, d, dl) 117 sys_munmap(d, 32) 118 return 0 119} 120// stderr twin of vc_n -- a refused capture announces its numbers on the error channel (R2) 121func vc_werr_n(v: i64) -> i64 { 122 let d: *u8 = sys_mmap(32) 123 let dl: i64 = mv_u_dec(d, 0, v) 124 sys_write(2, d, dl) 125 sys_munmap(d, 32) 126 return 0 127} 128 129func vc_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c } 130 131func vc_has_pfx(buf: *u8, pfx: *u8) -> i64 { 132 var i: i64 = 0 133 while pfx[i] != (0 as u8) { 134 if buf[i] != pfx[i] { return 0 } 135 i = i + 1 136 } 137 return 1 138} 139 140func vc_phys_of(ref: *u8, refroot: *u8, physroot: *u8, out: *u8, cap: i64) -> i64 { 141 if vc_has_pfx(ref, refroot) == 0 { 142 let d: i64 = vc_cat(out, 0, ref) 143 out[d] = 0 as u8 144 return d 145 } 146 let rl: i64 = vc_strlen(refroot) 147 var o: i64 = vc_cat(out, 0, physroot) 148 var i: i64 = rl 149 while ref[i] != (0 as u8) { out[o] = ref[i]; o = o + 1; i = i + 1 } 150 out[o] = 0 as u8 151 return o 152} 153 154func vc_conf_field(buf: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 { 155 var ls: i64 = 0 156 var i: i64 = 0 157 while i <= n { 158 var eol: i64 = 0 159 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } } 160 if eol == 1 { 161 var m: i64 = 1 162 var k: i64 = 0 163 while key[k] != (0 as u8) { 164 if ls + k >= i { m = 0 } else { if buf[ls + k] != key[k] { m = 0 } } 165 k = k + 1 166 } 167 if m == 1 { 168 var o: i64 = 0 169 var j: i64 = ls + k 170 while j < i { 171 if buf[j] != (13 as u8) { if o < cap - 1 { out[o] = buf[j]; o = o + 1 } } 172 j = j + 1 173 } 174 out[o] = 0 as u8 175 if o > 0 { return o } 176 } 177 ls = i + 1 178 } 179 i = i + 1 180 } 181 return 0 182} 183 184func vc_load_roots(ref_out: *u8, phys_out: *u8) -> i64 { 185 vc_cat(ref_out, 0, VC_REF_ROOT); ref_out[vc_strlen(VC_REF_ROOT)] = 0 as u8 186 vc_cat(phys_out, 0, VC_PHYS_ROOT); phys_out[vc_strlen(VC_PHYS_ROOT)] = 0 as u8 187 let szbox: *i64 = sys_mmap(16) as *i64 188 let buf: *u8 = sys_read_file(VC_ROOTS_CONF, szbox) 189 if (buf as i64) != 0 { 190 if szbox[0] > 0 { 191 vc_conf_field(buf, szbox[0], "ref_root=" as *u8, ref_out, VC_ROOT_CAP) 192 vc_conf_field(buf, szbox[0], "phys_root=" as *u8, phys_out, VC_ROOT_CAP) 193 } 194 } 195 sys_munmap(szbox as *u8, 16) 196 return 1 197} 198 199func vc_mkdirp(path: *u8) -> i64 { 200 let tmp: *u8 = sys_mmap(VC_PATH_CAP) 201 var n: i64 = 0 202 while path[n] != (0 as u8) { 203 if n < VC_PATH_CAP - 2 { tmp[n] = path[n] } 204 n = n + 1 205 } 206 if n >= VC_PATH_CAP - 2 { sys_munmap(tmp, VC_PATH_CAP); return 0 - 1 } 207 tmp[n] = 0 as u8 208 209 var last: i64 = 0 - 1 210 var i: i64 = 0 211 while i < n { if tmp[i] == (47 as u8) { last = i } i = i + 1 } 212 if last <= 0 { sys_munmap(tmp, VC_PATH_CAP); return 0 } 213 214 var j: i64 = 1 215 while j < last { 216 if tmp[j] == (47 as u8) { 217 tmp[j] = 0 as u8 218 sys_mkdir(tmp, 493) 219 tmp[j] = 47 as u8 220 } 221 j = j + 1 222 } 223 tmp[last] = 0 as u8 224 sys_mkdir(tmp, 493) 225 sys_munmap(tmp, VC_PATH_CAP) 226 return 1 227} 228 229func vc_ext_of(url: *u8, deflt: *u8, out: *u8, cap: i64) -> i64 { 230 let base: *u8 = sys_mmap(VC_URL_CAP) 231 let have: i64 = mvf_basename(url, base, VC_URL_CAP) 232 if have == 0 { 233 sys_munmap(base, VC_URL_CAP) 234 let d: i64 = vc_cat(out, 0, deflt) 235 out[d] = 0 as u8 236 return d 237 } 238 var n: i64 = 0 239 var cut: i64 = 0 - 1 240 while base[n] != (0 as u8) { 241 if cut < 0 { 242 if base[n] == (63 as u8) { cut = n } 243 if base[n] == (35 as u8) { cut = n } 244 } 245 n = n + 1 246 } 247 if cut >= 0 { base[cut] = 0 as u8; n = cut } 248 249 var dot: i64 = 0 - 1 250 var i: i64 = 0 251 while i < n { if base[i] == (46 as u8) { dot = i } i = i + 1 } 252 253 var ok: i64 = 1 254 if dot < 0 { ok = 0 } 255 if dot + 1 >= n { ok = 0 } 256 if n - dot - 1 >= cap { ok = 0 } 257 if n - dot - 1 > 12 { ok = 0 } 258 259 if ok == 1 { 260 var k: i64 = dot + 1 261 while k < n { 262 let c: i64 = base[k] as i64 263 var good: i64 = 0 264 if c >= 48 { if c <= 57 { good = 1 } } 265 if c >= 65 { if c <= 90 { good = 1 } } 266 if c >= 97 { if c <= 122 { good = 1 } } 267 if good == 0 { ok = 0 } 268 k = k + 1 269 } 270 } 271 272 if ok == 0 { 273 sys_munmap(base, VC_URL_CAP) 274 let d2: i64 = vc_cat(out, 0, deflt) 275 out[d2] = 0 as u8 276 return d2 277 } 278 var o: i64 = 0 279 var j: i64 = dot + 1 280 while j < n { out[o] = vc_lower(base[j] as i64) as u8; o = o + 1; j = j + 1 } 281 out[o] = 0 as u8 282 sys_munmap(base, VC_URL_CAP) 283 return o 284} 285 286func vc_ingest_file(store: *u8, ref_root: *u8, phys_root: *u8, tmppath: *u8, 287 source: *u8, ext: *u8, class_code: i64, 288 cidout: *u8, refout: *u8) -> i64 { 289 // R2 (/compare/mediaingest): disk-floor admission BEFORE this item is hashed and committed. The vault volume 290 // (phys_root) must hold at least the configured floor free; below it, or if the disk is UNMEASURABLE, REFUSE 291 // rather than grow the corpus onto a full volume. The refusal ANNOUNCES the two numbers -- a refusal that does 292 // not say how much was free and how much was needed is not actionable. The already-fetched tmp is unlinked so a 293 // refused capture leaves nothing behind. Callers read a negative return as a failure, which is correct. 294 let da: *i64 = sys_mmap(16) as *i64 295 let df: *i64 = sys_mmap(16) as *i64 296 let dv: i64 = ig_disk_floor(phys_root, da, df) 297 if dv != IG_ADMIT { 298 vc_werr("nx_vault_capture: ingest REFUSED -- " as *u8); vc_werr(ig_verdict_str(dv)) 299 vc_werr(" free=" as *u8); vc_werr_n(da[0]); vc_werr(" floor=" as *u8); vc_werr_n(df[0]); vc_werr("\n" as *u8) 300 sys_unlinkat(tmppath) 301 sys_munmap(da as *u8, 16) 302 sys_munmap(df as *u8, 16) 303 return VC_DISK_REFUSE 304 } 305 sys_munmap(da as *u8, 16) 306 sys_munmap(df as *u8, 16) 307 let szbox: *i64 = sys_mmap(16) as *i64 308 szbox[0] = 0 309 if cid_of_file_chunk(tmppath, CID_FILE_CHUNK, szbox, cidout) < 0 { 310 sys_munmap(szbox as *u8, 16) 311 return 0 - 1 312 } 313 314 let head: *u8 = sys_mmap(VC_HEAD_CAP) 315 let fd: i64 = sys_openat_rd(tmppath) 316 if fd < 0 { 317 sys_munmap(head, VC_HEAD_CAP) 318 sys_munmap(szbox as *u8, 16) 319 return 0 - 1 320 } 321 var hn: i64 = 0 322 var go: i64 = 1 323 while go == 1 { 324 let dst: *u8 = ((head as i64) + hn) as *u8 325 let r: i64 = sys_read(fd, dst, VC_HEAD_CAP - hn) 326 if r <= 0 { go = 0 } else { 327 hn = hn + r 328 if hn >= VC_HEAD_CAP { go = 0 } 329 } 330 } 331 sys_close(fd) 332 333 let vtype: i64 = mv_classify_plan(head, hn, class_code, MV_CTX_NONE, ref_root, 334 source, ext, refout, cidout) 335 sys_munmap(head, VC_HEAD_CAP) 336 337 let gp: *i64 = sys_mmap(16) as *i64 338 let gl: *i64 = sys_mmap(16) as *i64 339 let found: i64 = mv_store_get(store, cidout, gp, gl) 340 sys_munmap(gp as *u8, 16) 341 sys_munmap(gl as *u8, 16) 342 if found == 1 { 343 sys_unlinkat(tmppath) 344 sys_munmap(szbox as *u8, 16) 345 return 0 346 } 347 348 let phys: *u8 = sys_mmap(VC_PATH_CAP) 349 vc_phys_of(refout, ref_root, phys_root, phys, VC_PATH_CAP) 350 vc_mkdirp(phys) 351 if sys_renameat(tmppath, phys) < 0 { 352 sys_munmap(phys, VC_PATH_CAP) 353 sys_munmap(szbox as *u8, 16) 354 return 0 - 1 355 } 356 sys_munmap(phys, VC_PATH_CAP) 357 358 let rec: *u8 = sys_mmap(VC_REC_CAP) 359 let rl: i64 = mv_record_build(rec, class_code, vtype, MV_LVL_TS, source, refout) 360 let rc: i64 = mv_store_put(store, cidout, rec, rl) 361 sys_munmap(rec, VC_REC_CAP) 362 sys_munmap(szbox as *u8, 16) 363 if rc < 0 { return 0 - 1 } 364 return 1 365} 366 367// R0 (/compare/mediaingest, 2026-08-30): THE pacing hook. Every fetch this organ makes goes through it, before 368// (phase 0: the polite wait for the url's host, served by sleeping) and after (phase 1: the REAL status fed back so 369// a 429 or 503 becomes a longer next interval, persisted per host). It DELEGATES to nx_paced_fetch, which delegates 370// to nx_crawl_pace -- one ruler, no second copy. MEASURED before this hook: this organ imported no pacing at all, 371// so a site capture hit its host as fast as the asset loop ran. retry_after_s is 0 here on purpose: the fetch lib 372// hands back bytes and a status, not the headers, and a guessed Retry-After would be a fabricated measurement. 373func vc_pace_hook(url: *u8, phase: i64, status: i64) -> i64 { 374 if phase == 0 { return pf_before(url) } 375 return pf_after(url, status, 0) 376} 377 378func vc_fetch_tmp(store_i: i64, phys_root: *u8, url: *u8, seqn: i64, 379 out: *u8, stbox: *i64) -> i64 { 380 var o: i64 = vc_cat(out, 0, phys_root) 381 o = vc_cat(out, o, "/_incoming/cap" as *u8) 382 o = mv_u_dec(out, o, seqn) 383 out[o] = 0 as u8 384 vc_mkdirp(out) 385 let fd: i64 = sys_openat_wr(out, 420) 386 if fd < 0 { return 0 - 1 } 387 stbox[0] = 0 388 vc_pace_hook(url, 0, 0) 389 let n: i64 = hf_fetch_to_file(store_i, url, 0, 0, fd, 0, stbox) 390 vc_pace_hook(url, 1, stbox[0]) 391 sys_close(fd) 392 if n <= 0 { sys_unlinkat(out); return 0 - 1 } 393 if stbox[0] != 200 { sys_unlinkat(out); return 0 - 2 } 394 return n 395} 396 397func vc_capture_media(store_i: i64, store: *u8, ref_root: *u8, phys_root: *u8, 398 url: *u8, source: *u8, class_code: i64, seqn: i64, 399 cidout: *u8) -> i64 { 400 let tmp: *u8 = sys_mmap(VC_PATH_CAP) 401 let stbox: *i64 = sys_mmap(16) as *i64 402 let n: i64 = vc_fetch_tmp(store_i, phys_root, url, seqn, tmp, stbox) 403 sys_munmap(stbox as *u8, 16) 404 if n < 0 { sys_munmap(tmp, VC_PATH_CAP); return n } 405 406 let ext: *u8 = sys_mmap(VC_EXT_CAP) 407 vc_ext_of(url, "bin" as *u8, ext, VC_EXT_CAP) 408 let ref: *u8 = sys_mmap(VC_PATH_CAP) 409 let r: i64 = vc_ingest_file(store, ref_root, phys_root, tmp, source, ext, 410 class_code, cidout, ref) 411 sys_munmap(ext, VC_EXT_CAP) 412 sys_munmap(ref, VC_PATH_CAP) 413 sys_munmap(tmp, VC_PATH_CAP) 414 return r 415} 416 417func vc_items_add(items: *u8, io: i64, cid: *u8) -> i64 { 418 let cl: i64 = vc_strlen(cid) 419 if io + cl + 2 >= VC_ITEMS_CAP { return 0 - 1 } 420 var y: i64 = 0 421 while y < cl { items[io + y] = cid[y]; y = y + 1 } 422 items[io + cl] = 10 as u8 423 return io + cl + 1 424} 425 426func vc_seen(set: *u8, n: i64, hay: *u8) -> i64 { 427 let hl: i64 = vc_strlen(hay) 428 var ls: i64 = 0 429 var i: i64 = 0 430 while i <= n { 431 var eol: i64 = 0 432 if i == n { eol = 1 } else { if set[i] == (10 as u8) { eol = 1 } } 433 if eol == 1 { 434 if i - ls == hl { 435 var same: i64 = 1 436 var k: i64 = 0 437 while k < hl { if set[ls + k] != hay[k] { same = 0 } k = k + 1 } 438 if same == 1 { return 1 } 439 } 440 ls = i + 1 441 } 442 i = i + 1 443 } 444 return 0 445} 446 447func vc_page_core(store_i: i64, store: *u8, cpfx: *u8, ref_root: *u8, phys_root: *u8, 448 url: *u8, source: *u8, body: *u8, blen: i64, max_assets: i64, 449 site_col: *u8, page_col: *u8, stats: *i64, 450 links_out: *u8, links_cap: i64, links_len: *i64) -> i64 { 451 let items: *u8 = sys_mmap(VC_ITEMS_CAP) 452 var io: i64 = 0 453 454 let ptmp: *u8 = sys_mmap(VC_PATH_CAP) 455 var po: i64 = vc_cat(ptmp, 0, phys_root) 456 po = vc_cat(ptmp, po, "/_incoming/page" as *u8) 457 po = mv_u_dec(ptmp, po, stats[VC_ST_PAGES]) 458 ptmp[po] = 0 as u8 459 vc_mkdirp(ptmp) 460 let pcid: *u8 = sys_mmap(VC_CID_CAP) 461 let pfd: i64 = sys_openat_wr(ptmp, 420) 462 if pfd >= 0 { 463 sys_write(pfd, body, blen) 464 sys_close(pfd) 465 let pref: *u8 = sys_mmap(VC_PATH_CAP) 466 let pr: i64 = vc_ingest_file(store, ref_root, phys_root, ptmp, source, 467 "html" as *u8, MV_CLASS_REAL, pcid, pref) 468 sys_munmap(pref, VC_PATH_CAP) 469 if pr >= 0 { 470 stats[VC_ST_PAGE] = stats[VC_ST_PAGE] + 1 471 let r2: i64 = vc_items_add(items, io, pcid) 472 if r2 >= 0 { io = r2 } 473 } else { stats[VC_ST_FAIL] = stats[VC_ST_FAIL] + 1 } 474 } else { stats[VC_ST_FAIL] = stats[VC_ST_FAIL] + 1 } 475 sys_munmap(ptmp, VC_PATH_CAP) 476 477 let imgs: *u8 = sys_mmap(VC_LINKS_CAP) 478 let inn: i64 = mvf_extract(body, blen, "img" as *u8, "src" as *u8, 479 "" as *u8, imgs, VC_LINKS_CAP) 480 let one: *u8 = sys_mmap(VC_URL_CAP) 481 let href: *u8 = sys_mmap(VC_URL_CAP) 482 let acid: *u8 = sys_mmap(VC_CID_CAP) 483 var got: i64 = 0 484 485 if inn > 0 { 486 var ls: i64 = 0 487 var i: i64 = 0 488 while i <= inn { 489 var eol: i64 = 0 490 if i == inn { eol = 1 } else { if imgs[i] == (10 as u8) { eol = 1 } } 491 if eol == 1 { 492 let hl: i64 = i - ls 493 if hl > 0 { 494 if hl < VC_URL_CAP - 1 { 495 if got < max_assets { 496 stats[VC_ST_FOUND] = stats[VC_ST_FOUND] + 1 497 var q: i64 = 0 498 while q < hl { href[q] = imgs[ls + q]; q = q + 1 } 499 href[hl] = 0 as u8 500 if mvf_resolve(url, href, one, VC_URL_CAP) > 0 { 501 let seqn: i64 = stats[VC_ST_PAGES] * VC_MAGIC_4096 + got + 1 502 let r: i64 = vc_capture_media(store_i, store, ref_root, phys_root, 503 one, source, MV_CLASS_REAL, seqn, acid) 504 if r == 1 { 505 stats[VC_ST_NEW] = stats[VC_ST_NEW] + 1 506 let ra: i64 = vc_items_add(items, io, acid) 507 if ra >= 0 { io = ra } 508 got = got + 1 509 } 510 if r == 0 { 511 stats[VC_ST_DUP] = stats[VC_ST_DUP] + 1 512 let rd: i64 = vc_items_add(items, io, acid) 513 if rd >= 0 { io = rd } 514 got = got + 1 515 } 516 if r < 0 { stats[VC_ST_FAIL] = stats[VC_ST_FAIL] + 1 } 517 } else { stats[VC_ST_FAIL] = stats[VC_ST_FAIL] + 1 } 518 } 519 } 520 } 521 ls = i + 1 522 } 523 i = i + 1 524 } 525 } 526 sys_munmap(imgs, VC_LINKS_CAP) 527 528 if io > 0 { 529 mvc_batch_add_pfx(cpfx, page_col, items, io) 530 mvc_batch_add_pfx(cpfx, site_col, items, io) 531 } 532 sys_munmap(items, VC_ITEMS_CAP) 533 534 if links_cap > 0 { 535 links_len[0] = 0 536 let ln: i64 = mvf_extract(body, blen, "a" as *u8, "href" as *u8, 537 "" as *u8, links_out, links_cap) 538 if ln > 0 { if ln < links_cap { links_len[0] = ln } } 539 } 540 541 sys_munmap(one, VC_URL_CAP) 542 sys_munmap(href, VC_URL_CAP) 543 sys_munmap(acid, VC_CID_CAP) 544 sys_munmap(pcid, VC_CID_CAP) 545 return got 546} 547 548func vc_declare_page_cols(cpfx: *u8, url: *u8, host: *u8, 549 site_col: *u8, page_col: *u8) -> i64 { 550 mvc_declare_pfx(cpfx, "site" as *u8, host, host, host, url, "-" as *u8, site_col) 551 let pkey: *u8 = sys_mmap(VC_URL_CAP) 552 var i: i64 = 0 553 while url[i] != (0 as u8) { 554 if i < VC_URL_CAP - 1 { pkey[i] = url[i] } 555 i = i + 1 556 } 557 if i > VC_URL_CAP - 1 { i = VC_URL_CAP - 1 } 558 pkey[i] = 0 as u8 559 mvc_declare_pfx(cpfx, "page" as *u8, host, pkey, pkey, url, site_col, page_col) 560 sys_munmap(pkey, VC_URL_CAP) 561 return 1 562} 563 564func vc_report_stats(report: *u8, off: i64, stats: *i64) -> i64 { 565 var o: i64 = off 566 o = vc_cat(report, o, ",\"page_items\":" as *u8) 567 o = mv_u_dec(report, o, stats[VC_ST_PAGE]) 568 o = vc_cat(report, o, ",\"assets_found\":" as *u8) 569 o = mv_u_dec(report, o, stats[VC_ST_FOUND]) 570 o = vc_cat(report, o, ",\"stored\":" as *u8) 571 o = mv_u_dec(report, o, stats[VC_ST_NEW]) 572 o = vc_cat(report, o, ",\"dup\":" as *u8) 573 o = mv_u_dec(report, o, stats[VC_ST_DUP]) 574 o = vc_cat(report, o, ",\"failed\":" as *u8) 575 o = mv_u_dec(report, o, stats[VC_ST_FAIL]) 576 return o 577} 578 579func vc_capture_page(store_i: i64, store: *u8, cpfx: *u8, ref_root: *u8, phys_root: *u8, 580 url: *u8, source: *u8, max_assets: i64, report: *u8, rcap: i64) -> i64 { 581 let host: *u8 = sys_mmap(VC_HOST_CAP) 582 mvf_host_of(url, host) 583 584 let site_col: *u8 = sys_mmap(VC_CID_CAP) 585 let page_col: *u8 = sys_mmap(VC_CID_CAP) 586 vc_declare_page_cols(cpfx, url, host, site_col, page_col) 587 588 let page: *u8 = sys_mmap(VC_PAGE_CAP) 589 vc_pace_hook(url, 0, 0) 590 let pn: i64 = hf_fetch(store_i, url, 0, 0, page, VC_PAGE_CAP) 591 var pst: i64 = 0 592 if pn > 0 { pst = hf_status(page, pn) } 593 vc_pace_hook(url, 1, pst) 594 if pn <= 0 { sys_munmap(page, VC_PAGE_CAP); return 0 - 2 } 595 var boff: i64 = hf_body_off(page, pn) 596 if boff < 0 { boff = 0 } 597 let body: *u8 = ((page as i64) + boff) as *u8 598 599 let stats: *i64 = sys_mmap(8 * VC_ST_N) as *i64 600 var z: i64 = 0 601 while z < VC_ST_N { stats[z] = 0; z = z + 1 } 602 stats[VC_ST_PAGES] = 1 603 604 let lbox: *i64 = sys_mmap(16) as *i64 605 let got: i64 = vc_page_core(store_i, store, cpfx, ref_root, phys_root, url, source, 606 body, pn - boff, max_assets, site_col, page_col, stats, 607 "" as *u8, 0, lbox) 608 sys_munmap(page, VC_PAGE_CAP) 609 sys_munmap(lbox as *u8, 16) 610 611 var o: i64 = vc_cat(report, 0, "{\"scope\":\"page\",\"url\":\"" as *u8) 612 o = vc_cat(report, o, url) 613 o = vc_cat(report, o, "\",\"site_col\":\"" as *u8) 614 o = vc_cat(report, o, site_col) 615 o = vc_cat(report, o, "\",\"page_col\":\"" as *u8) 616 o = vc_cat(report, o, page_col) 617 o = vc_cat(report, o, "\"" as *u8) 618 o = vc_report_stats(report, o, stats) 619 o = vc_cat(report, o, "}" as *u8) 620 report[o] = 10 as u8 621 report[o + 1] = 0 as u8 622 623 sys_munmap(stats as *u8, 8 * VC_ST_N) 624 sys_munmap(host, VC_HOST_CAP) 625 sys_munmap(site_col, VC_CID_CAP) 626 sys_munmap(page_col, VC_CID_CAP) 627 return got 628} 629 630func vc_capture_site(store_i: i64, store: *u8, cpfx: *u8, ref_root: *u8, phys_root: *u8, 631 url: *u8, source: *u8, max_pages: i64, max_assets: i64, 632 report: *u8, rcap: i64) -> i64 { 633 let host: *u8 = sys_mmap(VC_HOST_CAP) 634 mvf_host_of(url, host) 635 636 let site_col: *u8 = sys_mmap(VC_CID_CAP) 637 mvc_declare_pfx(cpfx, "site" as *u8, host, host, host, url, "-" as *u8, site_col) 638 639 let frontier: *u8 = sys_mmap(VC_VISIT_CAP) 640 let visited: *u8 = sys_mmap(VC_VISIT_CAP) 641 var fo: i64 = vc_cat(frontier, 0, url) 642 frontier[fo] = 10 as u8 643 fo = fo + 1 644 var vo: i64 = 0 645 646 let stats: *i64 = sys_mmap(8 * VC_ST_N) as *i64 647 var z: i64 = 0 648 while z < VC_ST_N { stats[z] = 0; z = z + 1 } 649 650 let cur: *u8 = sys_mmap(VC_URL_CAP) 651 let one: *u8 = sys_mmap(VC_URL_CAP) 652 let href: *u8 = sys_mmap(VC_URL_CAP) 653 let lhost: *u8 = sys_mmap(VC_HOST_CAP) 654 let page_col: *u8 = sys_mmap(VC_CID_CAP) 655 let page: *u8 = sys_mmap(VC_PAGE_CAP) 656 let links: *u8 = sys_mmap(VC_LINKS_CAP) 657 let lbox: *i64 = sys_mmap(16) as *i64 658 659 var fp: i64 = 0 660 var pages: i64 = 0 661 var refused: i64 = 0 662 663 while fp < fo { 664 if pages >= max_pages { fp = fo } else { 665 var stop: i64 = 0 - 1 666 var q: i64 = fp 667 while q < fo { 668 if stop < 0 { if frontier[q] == (10 as u8) { stop = q } } 669 q = q + 1 670 } 671 if stop < 0 { stop = fo } 672 673 let cl: i64 = stop - fp 674 var ok: i64 = 1 675 if cl <= 0 { ok = 0 } 676 if cl >= VC_URL_CAP - 1 { ok = 0 } 677 678 if ok == 1 { 679 var k: i64 = 0 680 while k < cl { cur[k] = frontier[fp + k]; k = k + 1 } 681 cur[cl] = 0 as u8 682 if vc_seen(visited, vo, cur) == 1 { ok = 0 } 683 } 684 if ok == 1 { 685 if vo + cl + 2 < VC_VISIT_CAP { 686 var m: i64 = 0 687 while m < cl { visited[vo + m] = cur[m]; m = m + 1 } 688 visited[vo + cl] = 10 as u8 689 vo = vo + cl + 1 690 } else { ok = 0; refused = refused + 1 } 691 } 692 693 if ok == 1 { 694 vc_pace_hook(cur, 0, 0) 695 let pn: i64 = hf_fetch(store_i, cur, 0, 0, page, VC_PAGE_CAP) 696 var cst: i64 = 0 697 if pn > 0 { cst = hf_status(page, pn) } 698 vc_pace_hook(cur, 1, cst) 699 if pn <= 0 { stats[VC_ST_FAIL] = stats[VC_ST_FAIL] + 1 } else { 700 var boff: i64 = hf_body_off(page, pn) 701 if boff < 0 { boff = 0 } 702 let body: *u8 = ((page as i64) + boff) as *u8 703 704 pages = pages + 1 705 stats[VC_ST_PAGES] = pages 706 vc_declare_page_cols(cpfx, cur, host, site_col, page_col) 707 lbox[0] = 0 708 vc_page_core(store_i, store, cpfx, ref_root, phys_root, cur, source, 709 body, pn - boff, max_assets, site_col, page_col, stats, 710 links, VC_LINKS_CAP, lbox) 711 712 let ln: i64 = lbox[0] 713 var ls: i64 = 0 714 var i: i64 = 0 715 while i <= ln { 716 var eol: i64 = 0 717 if i == ln { eol = 1 } else { if links[i] == (10 as u8) { eol = 1 } } 718 if eol == 1 { 719 let hl: i64 = i - ls 720 if hl > 0 { 721 if hl < VC_URL_CAP - 1 { 722 var y: i64 = 0 723 while y < hl { href[y] = links[ls + y]; y = y + 1 } 724 href[hl] = 0 as u8 725 if mvf_resolve(cur, href, one, VC_URL_CAP) > 0 { 726 mvf_host_of(one, lhost) 727 if vc_eq(lhost, host) == 1 { 728 if vc_seen(visited, vo, one) == 0 { 729 if vc_seen(frontier, fo, one) == 0 { 730 let ul: i64 = vc_strlen(one) 731 if fo + ul + 2 < VC_VISIT_CAP { 732 var w: i64 = 0 733 while w < ul { frontier[fo + w] = one[w]; w = w + 1 } 734 frontier[fo + ul] = 10 as u8 735 fo = fo + ul + 1 736 } else { refused = refused + 1 } 737 } 738 } 739 } 740 } 741 } 742 } 743 ls = i + 1 744 } 745 i = i + 1 746 } 747 } 748 } 749 fp = stop + 1 750 } 751 } 752 753 var o: i64 = vc_cat(report, 0, "{\"scope\":\"site\",\"host\":\"" as *u8) 754 o = vc_cat(report, o, host) 755 o = vc_cat(report, o, "\",\"site_col\":\"" as *u8) 756 o = vc_cat(report, o, site_col) 757 o = vc_cat(report, o, "\",\"pages\":" as *u8) 758 o = mv_u_dec(report, o, pages) 759 o = vc_report_stats(report, o, stats) 760 o = vc_cat(report, o, ",\"budget_refused\":" as *u8) 761 o = mv_u_dec(report, o, refused) 762 o = vc_cat(report, o, "}" as *u8) 763 report[o] = 10 as u8 764 report[o + 1] = 0 as u8 765 766 sys_munmap(page, VC_PAGE_CAP) 767 sys_munmap(links, VC_LINKS_CAP) 768 sys_munmap(frontier, VC_VISIT_CAP) 769 sys_munmap(visited, VC_VISIT_CAP) 770 sys_munmap(stats as *u8, 8 * VC_ST_N) 771 sys_munmap(lbox as *u8, 16) 772 return pages 773} 774 775func vc_write_file(path: *u8, content: *u8, n: i64) -> i64 { 776 vc_mkdirp(path) 777 let fd: i64 = sys_openat_wr(path, 420) 778 if fd < 0 { return 0 - 1 } 779 sys_write(fd, content, n) 780 sys_close(fd) 781 return n 782} 783 784func do_selftest() -> i64 { 785 var fails: i64 = 0 786 let store: *u8 = "knowledge/store/vctest-" as *u8 787 let cpfx: *u8 = "knowledge/store/vctestc-" as *u8 788 let refroot: *u8 = "/volume1/vault" as *u8 789 let physroot: *u8 = "/tmp/vcphys" as *u8 790 791 let e: *u8 = sys_mmap(VC_EXT_CAP) 792 vc_ext_of("https://h/a/b/photo.JPG?x=1" as *u8, "bin" as *u8, e, VC_EXT_CAP) 793 if vc_eq(e, "jpg" as *u8) != 1 { fails = fails + 1 } 794 vc_w("T1 ext of photo.JPG?x=1 -> " as *u8); vc_w(e); vc_w("\n" as *u8) 795 796 vc_ext_of("https://cdn/stream/9f8a2b" as *u8, "bin" as *u8, e, VC_EXT_CAP) 797 if vc_eq(e, "bin" as *u8) != 1 { fails = fails + 1 } 798 vc_w("T2 extensionless CDN url -> " as *u8); vc_w(e); vc_w("\n" as *u8) 799 800 let tmp: *u8 = "/tmp/vcphys/_incoming/t3" as *u8 801 let png: *u8 = sys_mmap(64) 802 png[0] = 137 as u8; png[1] = 80 as u8; png[2] = 78 as u8; png[3] = 71 as u8 803 png[4] = 13 as u8; png[5] = 10 as u8; png[6] = 26 as u8; png[7] = 10 as u8 804 // ★ seed with the current epoch so each gate RUN produces a FRESH CID. 805 // A selftest that writes a fixed fixture into a persistent store is not 806 // repeatable: the SECOND run sees the FIRST run's record and every 807 // new-ingest assertion flips to dup. Uniqueness per run restores isolation. 808 let tnonce: i64 = sys_now_realtime_sec() 809 var f: i64 = 8 810 while f < 16 { png[f] = ((tnonce >> ((f - 8) * 8)) & 255) as u8; f = f + 1 } 811 while f < 64 { png[f] = f as u8; f = f + 1 } 812 vc_write_file(tmp, png, 64) 813 let cid: *u8 = sys_mmap(VC_CID_CAP) 814 let ref: *u8 = sys_mmap(VC_PATH_CAP) 815 let r1: i64 = vc_ingest_file(store, refroot, physroot, tmp, "test" as *u8, 816 "png" as *u8, MV_CLASS_REAL, cid, ref) 817 if r1 != 1 { fails = fails + 1 } 818 vc_w("T3 first ingest (want 1=new) -> " as *u8); vc_n(r1) 819 vc_w(" ref " as *u8); vc_w(ref); vc_w("\n" as *u8) 820 821 if vc_has_pfx(ref, refroot) != 1 { fails = fails + 1 } 822 if vc_has_pfx(ref, physroot) == 1 { fails = fails + 1 } 823 vc_w("T13 record ref is LOGICAL (/volume1/vault), not phys -> ok\n" as *u8) 824 825 let phys: *u8 = sys_mmap(VC_PATH_CAP) 826 vc_phys_of(ref, refroot, physroot, phys, VC_PATH_CAP) 827 let pchk: i64 = sys_openat_rd(phys) 828 if pchk < 0 { fails = fails + 1 } else { sys_close(pchk) } 829 let lchk: i64 = sys_openat_rd(ref) 830 if lchk >= 0 { fails = fails + 1; sys_close(lchk) } 831 vc_w("T14 blob at PHYSICAL " as *u8); vc_w(phys) 832 vc_w(" (fd " as *u8); vc_n(pchk); vc_w("), logical absent (fd " as *u8); vc_n(lchk); vc_w(")\n" as *u8) 833 834 if vc_has_pfx(phys, physroot) != 1 { fails = fails + 1 } 835 vc_w("T15 vc_phys_of round-trips ref->phys under phys_root -> ok\n" as *u8) 836 837 let passt: *u8 = sys_mmap(VC_PATH_CAP) 838 vc_phys_of("/some/other/path.png" as *u8, refroot, physroot, passt, VC_PATH_CAP) 839 if vc_eq(passt, "/some/other/path.png" as *u8) != 1 { fails = fails + 1 } 840 vc_w("T16 non-ref-root path passes through unchanged -> " as *u8); vc_w(passt); vc_w("\n" as *u8) 841 842 vc_write_file(tmp, png, 64) 843 let cid2: *u8 = sys_mmap(VC_CID_CAP) 844 let ref2: *u8 = sys_mmap(VC_PATH_CAP) 845 let r2: i64 = vc_ingest_file(store, refroot, physroot, tmp, "test" as *u8, 846 "png" as *u8, MV_CLASS_REAL, cid2, ref2) 847 if r2 != 0 { fails = fails + 1 } 848 if vc_eq(cid, cid2) != 1 { fails = fails + 1 } 849 vc_w("T5 re-ingest identical bytes (want 0=dup, same cid) -> " as *u8); vc_n(r2); vc_w("\n" as *u8) 850 851 let scol: *u8 = sys_mmap(VC_CID_CAP) 852 mvc_declare_pfx(cpfx, "site" as *u8, "t.example" as *u8, "t.example" as *u8, 853 "t.example" as *u8, "https://t.example" as *u8, "-" as *u8, scol) 854 let ib: *u8 = sys_mmap(VC_MAGIC_1024) 855 let io: i64 = vc_items_add(ib, 0, cid) 856 mvc_batch_add_pfx(cpfx, scol, ib, io) 857 858 let mem: *u8 = sys_mmap(VC_MAGIC_65536) 859 let mn: i64 = mvc_items_pfx(cpfx, scol, mem, VC_MAGIC_65536) 860 var members: i64 = 0 861 var resolved: i64 = 0 862 let gp: *i64 = sys_mmap(16) as *i64 863 let gl: *i64 = sys_mmap(16) as *i64 864 let mcid: *u8 = sys_mmap(VC_CID_CAP) 865 var ls: i64 = 0 866 var i: i64 = 0 867 while i <= mn { 868 var eol: i64 = 0 869 if i == mn { eol = 1 } else { if mem[i] == (10 as u8) { eol = 1 } } 870 if eol == 1 { 871 let hl: i64 = i - ls 872 if hl > 0 { 873 if hl < VC_CID_CAP - 1 { 874 members = members + 1 875 var k: i64 = 0 876 while k < hl { mcid[k] = mem[ls + k]; k = k + 1 } 877 mcid[hl] = 0 as u8 878 if mv_store_get(store, mcid, gp, gl) == 1 { resolved = resolved + 1 } 879 } 880 } 881 ls = i + 1 882 } 883 i = i + 1 884 } 885 if members == 0 { fails = fails + 1 } 886 if resolved != members { fails = fails + 1 } 887 vc_w("T6 INVARIANT every collection member has a record -> " as *u8) 888 vc_n(resolved); vc_w("/" as *u8); vc_n(members); vc_w("\n" as *u8) 889 890 let ghost: *u8 = "nxc1-0000000000000000000000000000000000000000000000000000000000000000" as *u8 891 if mv_store_get(store, ghost, gp, gl) == 1 { fails = fails + 1 } 892 vc_w("T7 NEG never-ingested cid does NOT resolve\n" as *u8) 893 894 let html: *u8 = "<html><body><img src=\"/a/one.png\"><a href=\"/next\">n</a><img src=\"https://cdn.x/two.jpg\"></body></html>" as *u8 895 let outb: *u8 = sys_mmap(VC_MAGIC_8192) 896 let an: i64 = mvf_extract(html, vc_strlen(html), "img" as *u8, "src" as *u8, 897 "" as *u8, outb, VC_MAGIC_8192) 898 let acount: i64 = mvc_count_lines(outb, an) 899 if acount != 2 { fails = fails + 1 } 900 vc_w("T8 img srcs extracted (want 2) -> " as *u8); vc_n(acount); vc_w("\n" as *u8) 901 902 let res: *u8 = sys_mmap(VC_URL_CAP) 903 mvf_resolve("https://t.example/dir/page.html" as *u8, "/a/one.png" as *u8, res, VC_URL_CAP) 904 if vc_eq(res, "https://t.example/a/one.png" as *u8) != 1 { fails = fails + 1 } 905 vc_w("T9 relative -> absolute: " as *u8); vc_w(res); vc_w("\n" as *u8) 906 907 // T17 -- protocol-relative //host/path inherits the base scheme (the fix for 908 // the 8/14 Pima_cotton asset failures; the dominant CDN url shape) 909 let pr: *u8 = sys_mmap(VC_URL_CAP) 910 mvf_resolve("https://en.wikipedia.org/wiki/X" as *u8, "//upload.wikimedia.org/a/b.jpg" as *u8, pr, VC_URL_CAP) 911 if vc_eq(pr, "https://upload.wikimedia.org/a/b.jpg" as *u8) != 1 { fails = fails + 1 } 912 vc_w("T17 protocol-relative -> " as *u8); vc_w(pr); vc_w("\n" as *u8) 913 914 let h1: *u8 = sys_mmap(VC_HOST_CAP) 915 let h2: *u8 = sys_mmap(VC_HOST_CAP) 916 mvf_host_of("https://t.example/a" as *u8, h1) 917 mvf_host_of("https://evil.other/a" as *u8, h2) 918 if vc_eq(h1, h2) == 1 { fails = fails + 1 } 919 if vc_eq(h1, "t.example" as *u8) != 1 { fails = fails + 1 } 920 vc_w("T10 same-host filter separates " as *u8); vc_w(h1) 921 vc_w(" from " as *u8); vc_w(h2); vc_w("\n" as *u8) 922 923 let small: *u8 = sys_mmap(64) 924 let over: i64 = vc_items_add(small, VC_ITEMS_CAP - 4, cid) 925 if over >= 0 { fails = fails + 1 } 926 vc_w("T11 over-envelope membership add REFUSES -> " as *u8); vc_n(over); vc_w("\n" as *u8) 927 928 let set: *u8 = "https://t.example/abc\n" as *u8 929 if vc_seen(set, 21, "https://t.example/ab" as *u8) == 1 { fails = fails + 1 } 930 if vc_seen(set, 21, "https://t.example/abc" as *u8) != 1 { fails = fails + 1 } 931 vc_w("T12 visited-set matches whole lines only\n" as *u8) 932 933 vc_w("verdict=" as *u8) 934 if fails == 0 { vc_w("GREEN 17/17\n" as *u8) } else { 935 vc_w("RED fails=" as *u8); vc_n(fails); vc_w("\n" as *u8) 936 } 937 if fails == 0 { return 0 } 938 return 1 939} 940 941func vc_usage() -> i64 { 942 vc_werr("usage:\n nx_vault_capture media <url> [source]\n nx_vault_capture edge <url> [source] (media, with the SSRF url guard in front -- for urls chosen by an untrusted page)\n nx_vault_capture page <url> [source] [max-assets]\n nx_vault_capture site <url> [source] [max-pages] [max-assets]\n nx_vault_capture selftest\n" as *u8) 943 return 2 944} 945 946func argp(argv: *i64, i: i64) -> *u8 { return (argv[i]) as *u8 } 947 948func vc_atoi(s: *u8) -> i64 { 949 var v: i64 = 0 950 var i: i64 = 0 951 while s[i] != (0 as u8) { 952 let c: i64 = s[i] as i64 953 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 954 i = i + 1 955 } 956 return v 957} 958 959func main(argc: i64, argv: *i64) -> i64 { 960 if argc < 2 { return vc_usage() } 961 let verb: *u8 = argp(argv, 1) 962 963 if vc_eq(verb, "selftest" as *u8) == 1 { return do_selftest() } 964 965 if argc < 3 { return vc_usage() } 966 let url: *u8 = argp(argv, 2) 967 968 let store: *u8 = "knowledge/store/mvault-" as *u8 969 let cpfx: *u8 = "knowledge/store/mvcoll-" as *u8 970 971 let ref_root: *u8 = sys_mmap(VC_ROOT_CAP) 972 let phys_root: *u8 = sys_mmap(VC_ROOT_CAP) 973 vc_load_roots(ref_root, phys_root) 974 975 var source: *u8 = "web" as *u8 976 if argc > 3 { source = argp(argv, 3) } 977 978 let store_i: i64 = hf_store_load() 979 if store_i <= 0 { 980 vc_werr("ERR: trust store load failed (data/mozilla_certdata.txt on CWD?)\n" as *u8) 981 return 3 982 } 983 984 let report: *u8 = sys_mmap(VC_REPORT_CAP) 985 986 // ---- THE UNTRUSTED DOOR (/compare/mediaingest R11 ge_edge_push) ------------------------------- 987 // `media` is the TRUSTED verb: an operator or an in-estate organ hands it a url it already vouches 988 // for, and its behaviour here is byte-unchanged. `edge` is the SAME capture with an SSRF admission 989 // check in front, and it exists because the browser-extension push hands this organ a url chosen by 990 // whatever page the user happened to be looking at. Fetching that unguarded is a server-side request 991 // forgery primitive pointed at our own LAN: this estate has already measured the shape once, when an 992 // unauthenticated SSRF-capable fetcher bound to INADDR_ANY answered from another host. 993 // The rule set is NOT re-implemented here -- it is nx_url_safety_lib, the one ruler shared with 994 // nx_clean_serve_daemon and nx_cleanview, so a host one surface learns to refuse is refused here too. 995 // The refusal NAMES WHICH RULE FIRED rather than saying "unsafe", because a guard whose refusals are 996 // indistinguishable cannot be told apart from a guard that refuses everything. 997 var do_media: i64 = 0 998 if vc_eq(verb, "media" as *u8) == 1 { do_media = 1 } 999 if vc_eq(verb, "edge" as *u8) == 1 { 1000 let ur: i64 = us_reason(url, vc_strlen(url)) 1001 if ur != US_OK { 1002 vc_werr("EDGE REFUSED url-unsafe rule=" as *u8) 1003 vc_werr(us_reason_name(ur)) 1004 vc_werr(" url=" as *u8) 1005 vc_werr(url) 1006 vc_werr(" -- refused BEFORE any fetch, so nothing on the private side was ever contacted\n" as *u8) 1007 return VC_EXIT_UNSAFE_URL 1008 } 1009 do_media = 1 1010 } 1011 1012 if do_media == 1 { 1013 let cid: *u8 = sys_mmap(VC_CID_CAP) 1014 let host: *u8 = sys_mmap(VC_HOST_CAP) 1015 mvf_host_of(url, host) 1016 let scol: *u8 = sys_mmap(VC_CID_CAP) 1017 mvc_declare_pfx(cpfx, "site" as *u8, host, host, host, url, "-" as *u8, scol) 1018 1019 let r: i64 = vc_capture_media(store_i, store, ref_root, phys_root, url, 1020 source, MV_CLASS_REAL, 1, cid) 1021 if r < 0 { 1022 vc_werr("ERR: capture failed (fetch or non-200)\n" as *u8) 1023 return 6 1024 } 1025 let ib: *u8 = sys_mmap(VC_MAGIC_1024) 1026 let io: i64 = vc_items_add(ib, 0, cid) 1027 if io > 0 { mvc_batch_add_pfx(cpfx, scol, ib, io) } 1028 1029 var o: i64 = vc_cat(report, 0, "{\"scope\":\"media\",\"cid\":\"" as *u8) 1030 o = vc_cat(report, o, cid) 1031 o = vc_cat(report, o, "\",\"site_col\":\"" as *u8) 1032 o = vc_cat(report, o, scol) 1033 o = vc_cat(report, o, "\",\"stored\":" as *u8) 1034 o = mv_u_dec(report, o, r) 1035 o = vc_cat(report, o, ",\"dup\":" as *u8) 1036 if r == 0 { o = mv_u_dec(report, o, 1) } else { o = mv_u_dec(report, o, 0) } 1037 o = vc_cat(report, o, "}" as *u8) 1038 report[o] = 10 as u8 1039 sys_write(1, report, o + 1) 1040 return 0 1041 } 1042 1043 if vc_eq(verb, "page" as *u8) == 1 { 1044 var ma: i64 = VC_DEF_ASSETS 1045 if argc > 4 { ma = vc_atoi(argp(argv, 4)) } 1046 if ma <= 0 { ma = VC_DEF_ASSETS } 1047 if ma > VC_MAX_ASSETS { ma = VC_MAX_ASSETS } 1048 let got: i64 = vc_capture_page(store_i, store, cpfx, ref_root, phys_root, url, 1049 source, ma, report, VC_REPORT_CAP) 1050 if got == (0 - 2) { 1051 vc_werr("ERR: page fetch failed\n" as *u8) 1052 return 6 1053 } 1054 sys_write(1, report, vc_strlen(report)) 1055 return 0 1056 } 1057 1058 if vc_eq(verb, "site" as *u8) == 1 { 1059 var mp: i64 = VC_DEF_PAGES 1060 if argc > 4 { mp = vc_atoi(argp(argv, 4)) } 1061 if mp <= 0 { mp = VC_DEF_PAGES } 1062 if mp > VC_MAX_PAGES { mp = VC_MAX_PAGES } 1063 var ma: i64 = VC_DEF_ASSETS 1064 if argc > 5 { ma = vc_atoi(argp(argv, 5)) } 1065 if ma <= 0 { ma = VC_DEF_ASSETS } 1066 if ma > VC_MAX_ASSETS { ma = VC_MAX_ASSETS } 1067 vc_capture_site(store_i, store, cpfx, ref_root, phys_root, url, source, 1068 mp, ma, report, VC_REPORT_CAP) 1069 sys_write(1, report, vc_strlen(report)) 1070 return 0 1071 } 1072 1073 return vc_usage() 1074}