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}