code wiki / _hdl_build / nx_roku_install.nx

nx_roku_install.nx source

↩ module page · 155 lines · 10627 B

1// nx_roku_install.nx -- SOVEREIGN Roku sideloader (R0b): pushes a channel zip onto a Roku in Developer Mode via 2// the device's Development Application Installer (HTTP Digest auth + multipart/form-data POST to /plugin_install). 3// This is the sovereign, third-party-free replacement for `curl --digest -F` and for the manual browser upload -- 4// it gives us the emit -> PUSH -> verify loop on the device (the live-testable-publish-loop standard). 5// Flow: TCP connect -> GET / to fetch the 401 Digest challenge -> parse realm+nonce -> compute response via the 6// KAT-proven nx_digest_auth (HA1=MD5(user:realm:pass), HA2=MD5(POST:/plugin_install), qop=auth) -> POST the zip 7// as multipart -> success iff the response carries "Install Success" / "Received" / "Identical". 8// Proven WITHOUT the device against a python HTTP-Digest mock (the 3rd-party oracle for this standard protocol). 9// nx_roku_install <ip> <port> <zippath> <user> <passfile> 10// e.g. nx_roku_install 192.168.8.50 80 /mnt/c/Users/elder/nishi_roku.zip rokudev /tmp/roku_pass.txt 11// license_tier: ORIGINAL 12import "nx_syscalls.nx" 13import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 14import "nx_digest_auth.nx" 15const K_MAGIC_8388608: i64 = 8388608 16const K_MAGIC_1024: i64 = 1024 17const K_MAGIC_131072: i64 = 131072 18 19func ri_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 20func ri_num(v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m;sys_write(1,"-" as *u8,1)}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=(48 as u8);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{bb[i]=t[k-1-i];i=i+1}; sys_write(1,bb,k); return 0 } 21func ri_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 22func ri_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ dst[off+i]=s[i]; i=i+1 } return off+i } 23func ri_catb(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 } 24func ri_catn(dst: *u8, off: i64, v: i64) -> i64 { var m: i64=v; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var o: i64=off; var i: i64=0; while i<k{dst[o]=t[k-1-i];o=o+1;i=i+1} return o } 25func ri_dump(buf: *u8, n: i64) -> i64 { var m: i64=n; if m>600 { m=600 } sys_write(1, buf, m); sys_write(1,"\n" as *u8,1); return 0 } 26 27func ri_read_file(path: *u8, buf: *u8, cap: i64) -> i64 { 28 let fd: i64=sys_openat_rd(path); if fd<0 { return 0-1 } 29 var total: i64=0; var nrd: i64=sys_read(fd, buf, cap) 30 while nrd>0 { total=total+nrd; if total>=cap { nrd=0 } else { nrd=sys_read(fd, ((buf as i64)+total) as *u8, cap-total) } } 31 sys_close(fd); return total 32} 33// parse dotted-quad into oct[0..3]; return 1 ok, 0 bad 34func ri_parse_ip(s: *u8, oct: *i64) -> i64 { 35 var idx: i64=0; var cur: i64=0; var i: i64=0 36 while s[i]!=(0 as u8) { 37 let ch: i64=s[i] as i64 38 if ch==46 { if idx>=3 { return 0 } oct[idx]=cur; idx=idx+1; cur=0 } 39 else { if ch>=48 { if ch<=57 { cur=cur*10+(ch-48) } else { return 0 } } else { return 0 } } 40 i=i+1 41 } 42 if idx!=3 { return 0 } 43 oct[3]=cur; return 1 44} 45func ri_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v } 46// 16-byte sockaddr_in 47func ri_sockaddr(buf: *u8, port: i64, a: i64, b: i64, c: i64, d: i64) -> i64 { 48 var i: i64=0; while i<16 { buf[i]=0 as u8; i=i+1 } 49 buf[0]=2 as u8 50 buf[2]=((port>>8)&0xff) as u8; buf[3]=(port&0xff) as u8 51 buf[4]=a as u8; buf[5]=b as u8; buf[6]=c as u8; buf[7]=d as u8 52 return 0 53} 54// find needle in hay[0..n); return index AFTER needle, or -1 55func ri_find(hay: *u8, n: i64, needle: *u8) -> i64 { 56 let nl: i64=ri_strlen(needle); if nl==0 { return 0-1 } 57 var i: i64=0 58 while i+nl<=n { var j: i64=0; var hit: i64=1; while j<nl { if hay[i+j]!=needle[j] { hit=0; j=nl } else { j=j+1 } } if hit==1 { return i+nl } i=i+1 } 59 return 0-1 60} 61func ri_contains(hay: *u8, n: i64, needle: *u8) -> i64 { if ri_find(hay,n,needle)>=0 { return 1 } return 0 } 62// extract a quoted value: key like `realm="` -> copy chars until the next '"' 63func ri_extract_q(hay: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 { 64 let start: i64=ri_find(hay, n, key); if start<0 { return 0-1 } 65 var i: i64=start; var o: i64=0 66 while i<n { if hay[i]==(0x22 as u8) { i=n } else { if o<cap { out[o]=hay[i]; o=o+1 } i=i+1 } } 67 out[o]=0 as u8; return o 68} 69// open TCP, send req, read entire response (Connection: close) into resp; return total bytes (or <0) 70func ri_http(a: i64, b: i64, c: i64, d: i64, port: i64, req: *u8, reqlen: i64, resp: *u8, cap: i64) -> i64 { 71 let fd: i64=sys_socket(AF_INET, SOCK_STREAM, 0); if fd<0 { return 0-1 } 72 sys_set_socket_timeout(fd, 30) 73 let dest: *u8=sys_mmap(16); ri_sockaddr(dest, port, a, b, c, d) 74 if nx_connect_bounded(fd, dest, 16, NX_CONN_DEFAULT_MS)!=0 { sys_close(fd); sys_munmap(dest,16); return 0-2 } 75 sys_write(fd, req, reqlen) 76 var total: i64=0; var r: i64=sys_read(fd, resp, cap) 77 while r>0 { total=total+r; if total>=cap { r=0 } else { r=sys_read(fd, ((resp as i64)+total) as *u8, cap-total) } } 78 sys_close(fd); sys_munmap(dest,16); return total 79} 80 81func main(argc: i64, argv: *i64) -> i64 { 82 if argc<6 { ri_puts("usage: nx_roku_install <ip> <port> <zippath> <user> <passfile>\n" as *u8); sys_exit(2); return 2 } 83 let ipstr: *u8=argv[1] as *u8; let portstr: *u8=argv[2] as *u8; let zippath: *u8=argv[3] as *u8 84 let user: *u8=argv[4] as *u8; let passfile: *u8=argv[5] as *u8 85 86 let oct: *i64=sys_mmap(64) as *i64 87 if ri_parse_ip(ipstr, oct)!=1 { ri_puts("BAD-IP (want dotted-quad)\n" as *u8); sys_exit(2); return 2 } 88 let port: i64=ri_atoi(portstr) 89 let ulen: i64=ri_strlen(user) 90 91 let zip: *u8=sys_mmap(K_MAGIC_8388608) 92 let ziplen: i64=ri_read_file(zippath, zip, K_MAGIC_8388608) 93 if ziplen<=0 { ri_puts("ZIP-READ-FAIL path=" as *u8); ri_puts(zippath); ri_puts("\n" as *u8); sys_exit(1); return 1 } 94 95 let passbuf: *u8=sys_mmap(256) 96 var plen: i64=ri_read_file(passfile, passbuf, 255) 97 if plen<0 { ri_puts("PASSFILE-READ-FAIL path=" as *u8); ri_puts(passfile); ri_puts("\n" as *u8); sys_exit(1); return 1 } 98 var stripping: i64=1 99 while stripping==1 { 100 if plen<=0 { stripping=0 } else { if passbuf[plen-1]==(10 as u8) { plen=plen-1 } else { if passbuf[plen-1]==(13 as u8) { plen=plen-1 } else { stripping=0 } } } 101 } 102 103 // -- 1) GET / to fetch the Digest challenge -- 104 let creq: *u8=sys_mmap(K_MAGIC_1024); var co: i64=0 105 co=ri_cat(creq, co, "GET / HTTP/1.1\r\nHost: " as *u8); co=ri_cat(creq, co, ipstr); co=ri_cat(creq, co, "\r\nConnection: close\r\n\r\n" as *u8) 106 let resp: *u8=sys_mmap(K_MAGIC_131072) 107 let rn: i64=ri_http(oct[0], oct[1], oct[2], oct[3], port, creq, co, resp, K_MAGIC_131072) 108 if rn<=0 { ri_puts("NO-RESPONSE rc=" as *u8); ri_num(rn); ri_puts(" -- is the Roku at " as *u8); ri_puts(ipstr); ri_puts(":" as *u8); ri_num(port); ri_puts(" with Developer Mode on?\n" as *u8); sys_exit(1); return 1 } 109 110 let realm: *u8=sys_mmap(256); let rl: i64=ri_extract_q(resp, rn, "realm=\"" as *u8, realm, 255) 111 let nonce: *u8=sys_mmap(256); let nl: i64=ri_extract_q(resp, rn, "nonce=\"" as *u8, nonce, 255) 112 if rl<0 { ri_puts("NO-DIGEST-CHALLENGE (no realm in 401). Got:\n" as *u8); ri_dump(resp, rn); sys_exit(1); return 1 } 113 if nl<0 { ri_puts("NO-DIGEST-CHALLENGE (no nonce in 401). Got:\n" as *u8); ri_dump(resp, rn); sys_exit(1); return 1 } 114 115 // -- 2) compute Digest response (qop=auth) via KAT-proven nx_digest_auth -- 116 let ha1: *u8=sys_mmap(64); digest_auth_ha1(user, ulen, realm, rl, passbuf, plen, ha1) 117 let ha2: *u8=sys_mmap(64); digest_auth_ha2("POST" as *u8, 4, "/plugin_install" as *u8, 15, ha2) 118 let cnonce: *u8="0a4f113b" as *u8 119 let respd: *u8=sys_mmap(64); digest_auth_response(ha1, nonce, nl, "00000001" as *u8, 8, cnonce, 8, "auth" as *u8, 4, ha2, respd) 120 121 // -- 3) build multipart body -- 122 let BND: *u8="NishiRokuBnd9f2a1c" as *u8 123 let body: *u8=sys_mmap(K_MAGIC_8388608); var bo: i64=0 124 bo=ri_cat(body, bo, "--" as *u8); bo=ri_cat(body, bo, BND); bo=ri_cat(body, bo, "\r\nContent-Disposition: form-data; name=\"mysubmit\"\r\n\r\nInstall\r\n" as *u8) 125 bo=ri_cat(body, bo, "--" as *u8); bo=ri_cat(body, bo, BND); bo=ri_cat(body, bo, "\r\nContent-Disposition: form-data; name=\"archive\"; filename=\"channel.zip\"\r\nContent-Type: application/zip\r\n\r\n" as *u8) 126 bo=ri_catb(body, bo, zip, ziplen) 127 bo=ri_cat(body, bo, "\r\n--" as *u8); bo=ri_cat(body, bo, BND); bo=ri_cat(body, bo, "--\r\n" as *u8) 128 let bodylen: i64=bo 129 130 // -- 4) POST /plugin_install with auth + body -- 131 let req: *u8=sys_mmap(K_MAGIC_8388608); var ro: i64=0 132 ro=ri_cat(req, ro, "POST /plugin_install HTTP/1.1\r\nHost: " as *u8); ro=ri_cat(req, ro, ipstr); ro=ri_cat(req, ro, "\r\n" as *u8) 133 ro=ri_cat(req, ro, "Authorization: Digest username=\"" as *u8); ro=ri_cat(req, ro, user) 134 ro=ri_cat(req, ro, "\", realm=\"" as *u8); ro=ri_cat(req, ro, realm) 135 ro=ri_cat(req, ro, "\", nonce=\"" as *u8); ro=ri_cat(req, ro, nonce) 136 ro=ri_cat(req, ro, "\", uri=\"/plugin_install\", qop=auth, nc=00000001, cnonce=\"" as *u8); ro=ri_cat(req, ro, cnonce) 137 ro=ri_cat(req, ro, "\", response=\"" as *u8); ro=ri_catb(req, ro, respd, 32); ro=ri_cat(req, ro, "\", algorithm=MD5\r\n" as *u8) 138 ro=ri_cat(req, ro, "Content-Type: multipart/form-data; boundary=" as *u8); ro=ri_cat(req, ro, BND) 139 ro=ri_cat(req, ro, "\r\nContent-Length: " as *u8); ro=ri_catn(req, ro, bodylen) 140 ro=ri_cat(req, ro, "\r\nConnection: close\r\n\r\n" as *u8) 141 ro=ri_catb(req, ro, body, bodylen) 142 143 let resp2: *u8=sys_mmap(K_MAGIC_131072) 144 let r2: i64=ri_http(oct[0], oct[1], oct[2], oct[3], port, req, ro, resp2, K_MAGIC_131072) 145 if r2<=0 { ri_puts("NO-RESPONSE-ON-UPLOAD rc=" as *u8); ri_num(r2); ri_puts("\n" as *u8); sys_exit(1); return 1 } 146 147 var ok: i64=0 148 if ri_contains(resp2, r2, "Install Success" as *u8)==1 { ok=1 } 149 if ri_contains(resp2, r2, "Application Received" as *u8)==1 { ok=1 } 150 if ri_contains(resp2, r2, "Received" as *u8)==1 { ok=1 } 151 if ri_contains(resp2, r2, "Identical" as *u8)==1 { ok=1 } 152 153 if ok==1 { ri_puts("ROKU-INSTALL-OK pushed=" as *u8); ri_num(ziplen); ri_puts("B to " as *u8); ri_puts(ipstr); ri_puts("\n" as *u8); sys_exit(0); return 0 } 154 ri_puts("ROKU-INSTALL-FAIL -- server response:\n" as *u8); ri_dump(resp2, r2); sys_exit(1); return 1 155}