diff --git a/Cargo.lock b/Cargo.lock index 1dee383..2230f35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,6 +345,7 @@ dependencies = [ "chrono-tz", "cpal", "crossterm", + "des", "dirs", "eframe", "flate2", @@ -1356,6 +1357,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/Cargo.toml b/Cargo.toml index 7e0529a..a126c34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ serde_json = "1" dirs = "7" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp", "tiff", "ico"] } base64 = "0.22" +# VNC sign-in (the 16-byte challenge) for people watching the bots' computer. +des = "0.8" chrono = "0.4" chrono-tz = "0.10" iana-time-zone = "0.1" diff --git a/mobile/index.html b/mobile/index.html index 74a12e5..f5027ad 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -272,7 +272,7 @@ a.plink{color:var(--accent);text-decoration:underline;text-underline-offset:3px;
-
+
這台 Mac
@@ -287,7 +287,6 @@ a.plink{color:var(--accent);text-decoration:underline;text-underline-offset:3px; -
@@ -295,6 +294,7 @@ a.plink{color:var(--accent);text-decoration:underline;text-underline-offset:3px;
Bot 需要你允許、或在你不在時做完事,手機會跳通知。
+ @@ -692,7 +692,7 @@ function renderAsk() { // 需要處理 (a step only you can do on a computer): its screen is one tap away, then 我完成了 / 跳過. if (a.kind === "handoff") { $("ask").innerHTML = `
需要處理 · ${esc(a.computer || "")}
${esc(a.reason)}
`; - $("ask").querySelector("[data-screen]").onclick = () => { S.back = S.view; show("screen"); $("seg").children[0].textContent = ownComputer(); $("seg").children[1].style.display = S.mac ? "" : "none"; startFrames(); }; + $("ask").querySelector("[data-screen]").onclick = () => openBox(true); } else { $("ask").innerHTML = `
${esc(a.title)}
${esc(a.reason)}
${a.can_always ? `` : ""}
`; } @@ -737,36 +737,33 @@ function renderChips() { // ---------- screens ---------- // Each bot's own computer goes by the bot's name (「Master 的電腦」). function ownComputer() { const n = botName(S.open); return n ? n + " 的電腦" : "Bot 的電腦"; } -$("b-screen").onclick = $("b-screen2").onclick = () => { S.back = S.view; show("screen"); $("seg").children[0].textContent = ownComputer(); $("seg").children[1].style.display = S.mac ? "" : "none"; startFrames(); }; +// The bots' computer: live in the viewer page (VNC), where you can also take over. +function openBox(control) { + const at = S.relay ? "host=" + encodeURIComponent(S.host) : "lan=1"; + location.href = "screen#" + at + "&name=" + encodeURIComponent(ownComputer()) + (control ? "&control=1" : ""); +} +$("b-screen").onclick = $("b-screen2").onclick = () => openBox(false); +$("m-mac").onclick = () => { $("sheet").classList.remove("on"); S.back = S.view; S.screen = "mac"; show("screen"); startFrames(); }; $("b-back2").onclick = () => show(S.back === "chat" ? "chat" : "list"); -document.querySelectorAll("#seg button").forEach((b) => b.onclick = () => { document.querySelectorAll("#seg button").forEach((x) => x.classList.toggle("on", x === b)); S.screen = b.dataset.w; $("k-wake").style.display = S.screen === "box" ? "" : "none"; startFrames(); }); -// The bots' computer sends a picture the moment its screen changes: ask for the one after the -// picture shown (X-Seq), again as soon as it arrives. (It was one picture a second, so a tap -// showed its result a second or two later.) This computer's own screen: a picture at a time. +// This Mac's screen: a picture at a time. let lastUrl = null; const nap = (ms) => new Promise((r) => setTimeout(r, ms)); function startFrames() { stopFrames(); const run = S.frameRun = {}; frames(run); } function stopFrames() { S.frameRun = null; } async function frames(run) { - let seq = 0; while (S.frameRun === run) { if (document.hidden) { await nap(800); continue; } try { - const live = S.screen === "box"; - const r = await api(base() + "api/screen/" + S.screen + (live ? "?after=" + seq : ""), {raw: true}); + const r = await api(base() + "api/screen/mac", {raw: true}); if (S.frameRun !== run) return; if (!r.ok) { const v = await r.json().catch(() => ({})); $("stage").innerHTML = `
${esc(v.error || "看不到畫面")}
`; $("screen-live").textContent = ""; await nap(1500); continue; } - const n = +(r.headers.get("X-Seq") || 0); - if (live && n && n === seq) continue; - seq = n; S.size = (r.headers.get("X-Size") || "0x0").split("x").map(Number); const url = URL.createObjectURL(await r.blob()); let img = $("stage").querySelector("img"); if (!img) { $("stage").innerHTML = ""; img = document.createElement("img"); img.draggable = false; $("stage").appendChild(img); bindStage(img); } img.src = url; if (lastUrl) URL.revokeObjectURL(lastUrl); lastUrl = url; $("screen-live").innerHTML = `即時`; - // At most about 8 pictures a second (an animated page would send all it can). - await nap(!live || !n ? 700 : 120); + await nap(700); } catch (e) { $("screen-live").textContent = ""; await nap(1500); } } } @@ -788,7 +785,7 @@ function act(img, e, button) { input2({kind: "click", x: p.x, y: p.y, button}); } async function input2(body) { - try { await api(base() + "api/screen/" + S.screen, {json: body}); } catch (e) { toast(e.message); } + try { await api(base() + "api/screen/mac", {json: body}); } catch (e) { toast(e.message); } } document.querySelectorAll("[data-key]").forEach((b) => b.onclick = () => input2({kind: "key", key: b.dataset.key})); document.querySelectorAll("[data-scroll]").forEach((b) => b.onclick = () => input2({kind: "scroll", x: Math.round(S.size[0] / 2), y: Math.round(S.size[1] / 2), direction: b.dataset.scroll, amount: 5})); @@ -796,9 +793,8 @@ $("k-type").onclick = () => { $("typebar").classList.toggle("on"); if ($("typeba $("b-type").onclick = () => { const t = $("typein").value; if (!t) return; $("typein").value = ""; input2({kind: "type", text: t}); }; $("typein").addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.isComposing) { e.preventDefault(); $("b-type").click(); } }); $("k-right").onclick = () => { S.right = !S.right; $("k-right").textContent = "右鍵:" + (S.right ? "開" : "關"); }; -$("k-wake").onclick = () => { toast("正在叫醒" + ownComputer() + "…"); input2({kind: "wake"}); }; // ---------- menu, notifications ---------- -$("b-more").onclick = () => { $("m-host").style.display = S.relay ? "" : "none"; $("m-pair").style.display = S.relay ? "" : "none"; notifyLabel(); $("sheet").classList.add("on"); }; +$("b-more").onclick = () => { $("m-mac").style.display = S.mac ? "" : "none"; $("m-host").style.display = S.relay ? "" : "none"; $("m-pair").style.display = S.relay ? "" : "none"; notifyLabel(); $("sheet").classList.add("on"); }; $("sheet").onclick = (e) => { if (e.target.id === "sheet") $("sheet").classList.remove("on"); }; $("m-cancel").onclick = () => $("sheet").classList.remove("on"); $("m-out").onclick = () => { $("sheet").classList.remove("on"); if (confirm("要登出這支手機嗎?")) signOut(); }; diff --git a/mobile/novnc.js b/mobile/novnc.js new file mode 100644 index 0000000..5c01ecd --- /dev/null +++ b/mobile/novnc.js @@ -0,0 +1,8 @@ +/* noVNC 1.6.0 (https://github.com/novnc/noVNC), core library under the Mozilla Public License 2.0 + (https://mozilla.org/MPL/2.0/; source: the tag v1.6.0, core/rfb.js and what it imports). + Bundled for AnyBot with: esbuild core/rfb.js --bundle --minify --format=esm --target=es2022 */ +function ji(i){return i>>>0}function it(i){return i|0}var qi="warn",L=()=>{},V=()=>{},W=()=>{},U=()=>{};function Zr(i){if(typeof i>"u"?i=qi:qi=i,L=V=W=U=()=>{},typeof window.console<"u")switch(i){case"debug":L=console.debug.bind(window.console);case"info":V=console.info.bind(window.console);case"warn":W=console.warn.bind(window.console);case"error":U=console.error.bind(window.console);case"none":break;default:throw new window.Error("invalid logging type '"+i+"'")}}Zr();function Bt(i,t=!1){try{return decodeURIComponent(escape(i))}catch(e){if(e instanceof URIError&&t)return i;throw e}}function he(i){return unescape(encodeURIComponent(i))}var Pe={toBase64Table:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split(""),base64Pad:"=",encode(i){"use strict";let t="",e=i.length,s=e%3;for(let n=0;n>2],t+=this.toBase64Table[((i[n]&3)<<4)+(i[n+1]>>4)],t+=this.toBase64Table[((i[n+1]&15)<<2)+(i[n+2]>>6)],t+=this.toBase64Table[i[n+2]&63];let r=e-s;return s===2?(t+=this.toBase64Table[i[r]>>2],t+=this.toBase64Table[((i[r]&3)<<4)+(i[r+1]>>4)],t+=this.toBase64Table[(i[r+1]&15)<<2],t+=this.toBase64Table[64]):s===1&&(t+=this.toBase64Table[i[r]>>2],t+=this.toBase64Table[(i[r]&3)<<4],t+=this.toBase64Table[64],t+=this.toBase64Table[64]),t},toBinaryTable:[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,0,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1],decode(i,t=0){let e=i.indexOf("=")-t;e<0&&(e=i.length-t);let s=(e>>2)*3+Math.floor(e%4/1.5),r=new Array(s),n=0,a=0;for(let h=0,_=t;_=8&&(n-=8,l||(r[h++]=a>>n&255),a&=(1<0||navigator.msMaxTouchPoints>0;window.addEventListener("touchstart",function i(){_i=!0,window.removeEventListener("touchstart",i,!1)},!1);var st=10*(window.devicePixelRatio||1),Ji=!1;try{let i=document.createElement("canvas");i.style.cursor='url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default',i.style.cursor.indexOf("url")===0?(V("Data URI scheme cursor supported"),Ji=!0):W("Data URI scheme cursor not supported")}catch(i){U("Data URI scheme cursor test exception: "+i)}var $i=Ji,Yr=!0;try{let i=document.createElement("div");i.style.visibility="hidden",i.style.overflow="scroll",document.body.appendChild(i);let t=document.createElement("div");i.appendChild(t);let e=i.offsetWidth-t.offsetWidth;i.parentNode.removeChild(i),Yr=e!=0}catch(i){U("Scrollbar test exception: "+i)}var fi=!1;async function jr(){if(!("VideoDecoder"in window))return!1;let i={codec:"avc1.42401f",codedWidth:1920,codedHeight:1080,optimizeForLatency:!0};if(!(await VideoDecoder.isConfigSupported(i)).supported)return!1;let e=new Uint8Array(Pe.decode("AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4HcRem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5ZjkgLSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIzIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw==")),s=!1,r=null,n=new VideoDecoder({output:h=>{s=!0},error:h=>{r=h}}),a=new EncodedVideoChunk({timestamp:0,type:"key",data:e});n.configure(i),n.decode(a);try{await n.flush()}catch(h){r=h}return!(!s||r!==null)}fi=await jr();function me(){return!!/mac/i.exec(navigator.platform)}function He(){return!!/win/i.exec(navigator.platform)}function Ue(){return!!/ipad/i.exec(navigator.platform)||!!/iphone/i.exec(navigator.platform)||!!/ipod/i.exec(navigator.platform)}function Ne(i,t,e){let s=e.getBoundingClientRect(),r={x:0,y:0};return i=s.right?r.x=s.width-1:r.x=i-s.left,t=s.bottom?r.y=s.height-1:r.y=t-s.top,r}function Se(i){i.stopPropagation(),i.preventDefault()}var ui=!1,ts=null;document.captureElement=null;function ke(i){if(ui)return;let t=new i.constructor(i.type,i);ui=!0,document.captureElement?document.captureElement.dispatchEvent(t):ts.dispatchEvent(t),ui=!1,i.stopPropagation(),t.defaultPrevented&&i.preventDefault(),i.type==="mouseup"&&ns()}function is(){let i=document.getElementById("noVNC_mouse_capture_elem");i.style.cursor=window.getComputedStyle(document.captureElement).cursor}var ss=new MutationObserver(is);function rs(i){if(i.setCapture)i.setCapture(),document.captureElement=i;else{ns();let t=document.getElementById("noVNC_mouse_capture_elem");t===null&&(t=document.createElement("div"),t.id="noVNC_mouse_capture_elem",t.style.position="fixed",t.style.top="0px",t.style.left="0px",t.style.width="100%",t.style.height="100%",t.style.zIndex=1e4,t.style.display="none",document.body.appendChild(t),t.addEventListener("contextmenu",ke),t.addEventListener("mousemove",ke),t.addEventListener("mouseup",ke)),document.captureElement=i,ss.observe(i,{attributes:!0}),is(),t.style.display="",window.addEventListener("mousemove",ke),window.addEventListener("mouseup",ke)}}function ns(){if(document.releaseCapture)document.releaseCapture(),document.captureElement=null;else{if(!document.captureElement)return;ts=document.captureElement,document.captureElement=null,ss.disconnect();let i=document.getElementById("noVNC_mouse_capture_elem");i.style.display="none",window.removeEventListener("mousemove",ke),window.removeEventListener("mouseup",ke)}}var Ee=class{constructor(){this._listeners=new Map}addEventListener(t,e){this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(e)}removeEventListener(t,e){this._listeners.has(t)&&this._listeners.get(t).delete(e)}dispatchEvent(t){return this._listeners.has(t.type)?(this._listeners.get(t.type).forEach(e=>e.call(this,t)),!t.defaultPrevented):!0}};var rt=class{constructor(t){if(this._drawCtx=null,this._renderQ=[],this._flushPromise=null,this._fbWidth=0,this._fbHeight=0,this._prevDrawStyle="",L(">> Display.constructor"),this._target=t,!this._target)throw new Error("Target must be set");if(typeof this._target=="string")throw new Error("target must be a DOM element");if(!this._target.getContext)throw new Error("no getContext method");this._targetCtx=this._target.getContext("2d"),this._viewportLoc={x:0,y:0,w:this._target.width,h:this._target.height},this._backbuffer=document.createElement("canvas"),this._drawCtx=this._backbuffer.getContext("2d"),this._damageBounds={left:0,top:0,right:this._backbuffer.width,bottom:this._backbuffer.height},L("User Agent: "+navigator.userAgent),L("<< Display.constructor"),this._scale=1,this._clipViewport=!1}get scale(){return this._scale}set scale(t){this._rescale(t)}get clipViewport(){return this._clipViewport}set clipViewport(t){this._clipViewport=t;let e=this._viewportLoc;this.viewportChangeSize(e.w,e.h),this.viewportChangePos(0,0)}get width(){return this._fbWidth}get height(){return this._fbHeight}viewportChangePos(t,e){let s=this._viewportLoc;t=Math.floor(t),e=Math.floor(e),this._clipViewport||(t=-s.w,e=-s.h);let r=s.x+s.w-1,n=s.y+s.h-1;t<0&&s.x+t<0&&(t=-s.x),r+t>=this._fbWidth&&(t-=r+t-this._fbWidth+1),s.y+e<0&&(e=-s.y),n+e>=this._fbHeight&&(e-=n+e-this._fbHeight+1),!(t===0&&e===0)&&(L("viewportChange deltaX: "+t+", deltaY: "+e),s.x+=t,s.y+=e,this._damage(s.x,s.y,s.w,s.h),this.flip())}viewportChangeSize(t,e){(!this._clipViewport||typeof t>"u"||typeof e>"u")&&(L("Setting viewport to full display region"),t=this._fbWidth,e=this._fbHeight),t=Math.floor(t),e=Math.floor(e),t>this._fbWidth&&(t=this._fbWidth),e>this._fbHeight&&(e=this._fbHeight);let s=this._viewportLoc;if(s.w!==t||s.h!==e){s.w=t,s.h=e;let r=this._target;r.width=t,r.height=e,this.viewportChangePos(0,0),this._damage(s.x,s.y,s.w,s.h),this.flip(),this._rescale(this._scale)}}absX(t){return this._scale===0?0:it(t/this._scale+this._viewportLoc.x)}absY(t){return this._scale===0?0:it(t/this._scale+this._viewportLoc.y)}resize(t,e){this._prevDrawStyle="",this._fbWidth=t,this._fbHeight=e;let s=this._backbuffer;if(s.width!==t||s.height!==e){let n=null;s.width>0&&s.height>0&&(n=this._drawCtx.getImageData(0,0,s.width,s.height)),s.width!==t&&(s.width=t),s.height!==e&&(s.height=e),n&&this._drawCtx.putImageData(n,0,0)}let r=this._viewportLoc;this.viewportChangeSize(r.w,r.h),this.viewportChangePos(0,0)}getImageData(){return this._drawCtx.getImageData(0,0,this.width,this.height)}toDataURL(t,e){return this._backbuffer.toDataURL(t,e)}toBlob(t,e,s){return this._backbuffer.toBlob(t,e,s)}_damage(t,e,s,r){tthis._damageBounds.right&&(this._damageBounds.right=t+s),e+r>this._damageBounds.bottom&&(this._damageBounds.bottom=e+r)}flip(t){if(this._renderQ.length!==0&&!t)this._renderQPush({type:"flip"});else{let e=this._damageBounds.left,s=this._damageBounds.top,r=this._damageBounds.right-e,n=this._damageBounds.bottom-s,a=e-this._viewportLoc.x,h=s-this._viewportLoc.y;a<0&&(r+=a,e-=a,a=0),h<0&&(n+=h,s-=h,h=0),a+r>this._viewportLoc.w&&(r=this._viewportLoc.w-a),h+n>this._viewportLoc.h&&(n=this._viewportLoc.h-h),r>0&&n>0&&this._targetCtx.drawImage(this._backbuffer,e,s,r,n,a,h,r,n),this._damageBounds.left=this._damageBounds.top=65535,this._damageBounds.right=this._damageBounds.bottom=0}}pending(){return this._renderQ.length>0}flush(){return this._renderQ.length===0?Promise.resolve():(this._flushPromise===null&&(this._flushPromise=new Promise(t=>{this._flushResolve=t})),this._flushPromise)}fillRect(t,e,s,r,n,a){this._renderQ.length!==0&&!a?this._renderQPush({type:"fill",x:t,y:e,width:s,height:r,color:n}):(this._setFillColor(n),this._drawCtx.fillRect(t,e,s,r),this._damage(t,e,s,r))}copyImage(t,e,s,r,n,a,h){this._renderQ.length!==0&&!h?this._renderQPush({type:"copy",oldX:t,oldY:e,x:s,y:r,width:n,height:a}):(this._drawCtx.mozImageSmoothingEnabled=!1,this._drawCtx.webkitImageSmoothingEnabled=!1,this._drawCtx.msImageSmoothingEnabled=!1,this._drawCtx.imageSmoothingEnabled=!1,this._drawCtx.drawImage(this._backbuffer,t,e,n,a,s,r,n,a),this._damage(s,r,n,a))}imageRect(t,e,s,r,n,a){if(s===0||r===0)return;let h=new Image;h.src="data: "+n+";base64,"+Pe.encode(a),this._renderQPush({type:"img",img:h,x:t,y:e,width:s,height:r})}videoFrame(t,e,s,r,n){this._renderQPush({type:"frame",frame:n,x:t,y:e,width:s,height:r})}blitImage(t,e,s,r,n,a,h){if(this._renderQ.length!==0&&!h){let _=new Uint8Array(s*r*4);_.set(new Uint8Array(n.buffer,0,_.length)),this._renderQPush({type:"blit",data:_,x:t,y:e,width:s,height:r})}else{let _=new Uint8ClampedArray(n.buffer,n.byteOffset+a,s*r*4),o=new ImageData(_,s,r);this._drawCtx.putImageData(o,t,e),this._damage(t,e,s,r)}}drawImage(t,...e){if(this._drawCtx.drawImage(t,...e),e.length<=4){let[s,r]=e;this._damage(s,r,t.width,t.height)}else{let[,,s,r,n,a]=e;this._damage(n,a,s,r)}}autoscale(t,e){let s;if(t===0||e===0)s=0;else{let r=this._viewportLoc,n=t/e;r.w/r.h>=n?s=t/r.w:s=e/r.h}this._rescale(s)}_rescale(t){this._scale=t;let e=this._viewportLoc,s=t*e.w+"px",r=t*e.h+"px";(this._target.style.width!==s||this._target.style.height!==r)&&(this._target.style.width=s,this._target.style.height=r)}_setFillColor(t){let e="rgb("+t[0]+","+t[1]+","+t[2]+")";e!==this._prevDrawStyle&&(this._drawCtx.fillStyle=e,this._prevDrawStyle=e)}_renderQPush(t){this._renderQ.push(t),this._renderQ.length===1&&this._scanRenderQ()}_resumeRenderQ(){this.removeEventListener("load",this._noVNCDisplay._resumeRenderQ),this._noVNCDisplay._scanRenderQ()}_scanRenderQ(){let t=!0;for(;t&&this._renderQ.length>0;){let e=this._renderQ[0];switch(e.type){case"flip":this.flip(!0);break;case"copy":this.copyImage(e.oldX,e.oldY,e.x,e.y,e.width,e.height,!0);break;case"fill":this.fillRect(e.x,e.y,e.width,e.height,e.color,!0);break;case"blit":this.blitImage(e.x,e.y,e.width,e.height,e.data,0,!0);break;case"img":if(e.img.complete){if(e.img.width!==e.width||e.img.height!==e.height){U("Decoded image has incorrect dimensions. Got "+e.img.width+"x"+e.img.height+". Expected "+e.width+"x"+e.height+".");return}this.drawImage(e.img,e.x,e.y)}else e.img._noVNCDisplay=this,e.img.addEventListener("load",this._resumeRenderQ),t=!1;break;case"frame":if(e.frame.ready){let s=e.frame.frame;(s.codedWidth{s._scanRenderQ()}),t=!1}break}t&&this._renderQ.shift()}this._renderQ.length===0&&this._flushPromise!==null&&(this._flushResolve(),this._flushPromise=null,this._flushResolve=null)}};function ae(i,t,e,s,r){if(t.subarray&&i.subarray){i.set(t.subarray(e,e+s),r);return}for(var n=0;n>>16&65535|0,a=0;e!==0;){a=e>2e3?2e3:e,e-=a;do r=r+t[s++]|0,n=n+r|0;while(--a);r%=65521,n%=65521}return r|n<<16|0}function G(){for(var i,t=[],e=0;e<256;e++){i=e;for(var s=0;s<8;s++)i=i&1?3988292384^i>>>1:i>>>1;t[e]=i}return t}var Ma=G();var Qt=30,qr=12;function di(i,t){var e,s,r,n,a,h,_,o,l,x,u,d,X,F,A,k,T,E,K,C,I,S,D,B,M;e=i.state,s=i.next_in,B=i.input,r=s+(i.avail_in-5),n=i.next_out,M=i.output,a=n-(t-i.avail_out),h=n+(i.avail_out-257),_=e.dmax,o=e.wsize,l=e.whave,x=e.wnext,u=e.window,d=e.hold,X=e.bits,F=e.lencode,A=e.distcode,k=(1<>>24,d>>>=K,X-=K,K=E>>>16&255,K===0)M[n++]=E&65535;else if(K&16){C=E&65535,K&=15,K&&(X>>=K,X-=K),X<15&&(d+=B[s++]<>>24,d>>>=K,X-=K,K=E>>>16&255,K&16){if(I=E&65535,K&=15,X_){i.msg="invalid distance too far back",e.mode=Qt;break e}if(d>>>=K,X-=K,K=n-a,I>K){if(K=I-K,K>l&&e.sane){i.msg="invalid distance too far back",e.mode=Qt;break e}if(S=0,D=u,x===0){if(S+=o-K,K2;)M[n++]=D[S++],M[n++]=D[S++],M[n++]=D[S++],C-=3;C&&(M[n++]=D[S++],C>1&&(M[n++]=D[S++]))}else{S=n-I;do M[n++]=M[S++],M[n++]=M[S++],M[n++]=M[S++],C-=3;while(C>2);C&&(M[n++]=M[S++],C>1&&(M[n++]=M[S++]))}}else if(K&64){i.msg="invalid distance code",e.mode=Qt;break e}else{E=A[(E&65535)+(d&(1<>3,s-=C,X-=C<<3,d&=(1<=1&&z[u]===0;u--);if(d>u&&(d=u),u===0)return r[n++]=1<<24|64<<16|0,r[n++]=1<<24|64<<16|0,h.bits=1,0;for(x=1;x0&&(i===hs||u!==1))return-1;for(te[1]=0,o=1;oas||i===ls&&k>os)return 1;for(;;){Fe=o-F,a[l]M?(_e=Ie[oe+a[l]],Z=D[B+a[l]]):(_e=96,Z=0),E=1<>F)+K]=Fe<<24|_e<<16|Z|0;while(K!==0);for(E=1<>=1;if(E!==0?(T&=E-1,T+=E):T=0,l++,--z[o]===0){if(o===u)break;o=t[e+a[l]]}if(o>d&&(T&I)!==C){for(F===0&&(F=d),S+=x,X=o-F,A=1<as||i===ls&&k>os)return 1;C=T&I,r[C]=d<<24|X<<16|S-n|0}}return T!==0&&(r[S+T]=o-F<<24|64<<16|0),h.bits=d,0}var sn=0,Is=1,Ps=2,cs=4,rn=5,It=6,Ut=0,nn=1,an=2,Ce=-2,on=-3,_s=-4,hn=-5,fs=8,Hs=1,us=2,ds=3,xs=4,ps=5,gs=6,bs=7,ms=8,ws=9,vs=10,ys=11,pe=12,pi=13,Xs=14,gi=15,Ks=16,Fs=17,ks=18,Ss=19,Pt=20,Ht=21,Es=22,As=23,Cs=24,Ls=25,Ts=26,bi=27,Ms=28,Rs=29,N=30,Bs=31,ln=32,cn=852,_n=592,fn=15,un=fn;function Ds(i){return(i>>>24&255)+(i>>>8&65280)+((i&65280)<<8)+((i&255)<<24)}function dn(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new ie(320),this.work=new ie(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function xn(i){var t;return!i||!i.state?Ce:(t=i.state,i.total_in=i.total_out=t.total=0,i.msg="",t.wrap&&(i.adler=t.wrap&1),t.mode=Hs,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new nt(cn),t.distcode=t.distdyn=new nt(_n),t.sane=1,t.back=-1,Ut)}function vi(i){var t;return!i||!i.state?Ce:(t=i.state,t.wsize=0,t.whave=0,t.wnext=0,xn(i))}function pn(i,t){var e,s;return!i||!i.state||(s=i.state,t<0?(e=0,t=-t):(e=(t>>4)+1,t<48&&(t&=15)),t&&(t<8||t>15))?Ce:(s.window!==null&&s.wbits!==t&&(s.window=null),s.wrap=e,s.wbits=t,vi(i))}function gn(i,t){var e,s;return i?(s=new dn,i.state=s,s.window=null,e=pn(i,t),e!==Ut&&(i.state=null),e):Ce}function Us(i){return gn(i,un)}var Qs=!0,mi,wi;function bn(i){if(Qs){var t;for(mi=new nt(512),wi=new nt(32),t=0;t<144;)i.lens[t++]=8;for(;t<256;)i.lens[t++]=9;for(;t<280;)i.lens[t++]=7;for(;t<288;)i.lens[t++]=8;for(Ae(Is,i.lens,0,288,mi,0,i.work,{bits:9}),t=0;t<32;)i.lens[t++]=5;Ae(Ps,i.lens,0,32,wi,0,i.work,{bits:5}),Qs=!1}i.lencode=mi,i.lenbits=9,i.distcode=wi,i.distbits=5}function mn(i,t,e,s){var r,n=i.state;return n.window===null&&(n.wsize=1<=n.wsize?(ae(n.window,t,e-n.wsize,n.wsize,0),n.wnext=0,n.whave=n.wsize):(r=n.wsize-n.wnext,r>s&&(r=s),ae(n.window,t,e-s,r,n.wnext),s-=r,s?(ae(n.window,t,e-s,s,0),n.wnext=s,n.whave=n.wsize):(n.wnext+=r,n.wnext===n.wsize&&(n.wnext=0),n.whave>>8&255,e.check=G(e.check,B,2,0),o=0,l=0,e.mode=us;break}if(e.flags=0,e.head&&(e.head.done=!1),!(e.wrap&1)||(((o&255)<<8)+(o>>8))%31){i.msg="incorrect header check",e.mode=N;break}if((o&15)!==fs){i.msg="unknown compression method",e.mode=N;break}if(o>>>=4,l-=4,S=(o&15)+8,e.wbits===0)e.wbits=S;else if(S>e.wbits){i.msg="invalid window size",e.mode=N;break}e.dmax=1<>8&1),e.flags&512&&(B[0]=o&255,B[1]=o>>>8&255,e.check=G(e.check,B,2,0)),o=0,l=0,e.mode=ds;case ds:for(;l<32;){if(h===0)break e;h--,o+=s[n++]<>>8&255,B[2]=o>>>16&255,B[3]=o>>>24&255,e.check=G(e.check,B,4,0)),o=0,l=0,e.mode=xs;case xs:for(;l<16;){if(h===0)break e;h--,o+=s[n++]<>8),e.flags&512&&(B[0]=o&255,B[1]=o>>>8&255,e.check=G(e.check,B,2,0)),o=0,l=0,e.mode=ps;case ps:if(e.flags&1024){for(;l<16;){if(h===0)break e;h--,o+=s[n++]<>>8&255,e.check=G(e.check,B,2,0)),o=0,l=0}else e.head&&(e.head.extra=null);e.mode=gs;case gs:if(e.flags&1024&&(d=e.length,d>h&&(d=h),d&&(e.head&&(S=e.head.extra_len-e.length,e.head.extra||(e.head.extra=new Array(e.head.extra_len)),ae(e.head.extra,s,n,d,S)),e.flags&512&&(e.check=G(e.check,s,d,n)),h-=d,n+=d,e.length-=d),e.length))break e;e.length=0,e.mode=bs;case bs:if(e.flags&2048){if(h===0)break e;d=0;do S=s[n+d++],e.head&&S&&e.length<65536&&(e.head.name+=String.fromCharCode(S));while(S&&d>9&1,e.head.done=!0),i.adler=e.check=0,e.mode=pe;break;case vs:for(;l<32;){if(h===0)break e;h--,o+=s[n++]<>>=l&7,l-=l&7,e.mode=bi;break}for(;l<3;){if(h===0)break e;h--,o+=s[n++]<>>=1,l-=1,o&3){case 0:e.mode=Xs;break;case 1:if(bn(e),e.mode=Pt,t===It){o>>>=2,l-=2;break e}break;case 2:e.mode=Fs;break;case 3:i.msg="invalid block type",e.mode=N}o>>>=2,l-=2;break;case Xs:for(o>>>=l&7,l-=l&7;l<32;){if(h===0)break e;h--,o+=s[n++]<>>16^65535)){i.msg="invalid stored block lengths",e.mode=N;break}if(e.length=o&65535,o=0,l=0,e.mode=gi,t===It)break e;case gi:e.mode=Ks;case Ks:if(d=e.length,d){if(d>h&&(d=h),d>_&&(d=_),d===0)break e;ae(r,s,n,d,a),h-=d,n+=d,_-=d,a+=d,e.length-=d;break}e.mode=pe;break;case Fs:for(;l<14;){if(h===0)break e;h--,o+=s[n++]<>>=5,l-=5,e.ndist=(o&31)+1,o>>>=5,l-=5,e.ncode=(o&15)+4,o>>>=4,l-=4,e.nlen>286||e.ndist>30){i.msg="too many length or distance symbols",e.mode=N;break}e.have=0,e.mode=ks;case ks:for(;e.have>>=3,l-=3}for(;e.have<19;)e.lens[te[e.have++]]=0;if(e.lencode=e.lendyn,e.lenbits=7,M={bits:e.lenbits},D=Ae(sn,e.lens,0,19,e.lencode,0,e.work,M),e.lenbits=M.bits,D){i.msg="invalid code lengths set",e.mode=N;break}e.have=0,e.mode=Ss;case Ss:for(;e.have>>24,T=A>>>16&255,E=A&65535,!(k<=l);){if(h===0)break e;h--,o+=s[n++]<>>=k,l-=k,e.lens[e.have++]=E;else{if(E===16){for(z=k+2;l>>=k,l-=k,e.have===0){i.msg="invalid bit length repeat",e.mode=N;break}S=e.lens[e.have-1],d=3+(o&3),o>>>=2,l-=2}else if(E===17){for(z=k+3;l>>=k,l-=k,S=0,d=3+(o&7),o>>>=3,l-=3}else{for(z=k+7;l>>=k,l-=k,S=0,d=11+(o&127),o>>>=7,l-=7}if(e.have+d>e.nlen+e.ndist){i.msg="invalid bit length repeat",e.mode=N;break}for(;d--;)e.lens[e.have++]=S}}if(e.mode===N)break;if(e.lens[256]===0){i.msg="invalid code -- missing end-of-block",e.mode=N;break}if(e.lenbits=9,M={bits:e.lenbits},D=Ae(Is,e.lens,0,e.nlen,e.lencode,0,e.work,M),e.lenbits=M.bits,D){i.msg="invalid literal/lengths set",e.mode=N;break}if(e.distbits=6,e.distcode=e.distdyn,M={bits:e.distbits},D=Ae(Ps,e.lens,e.nlen,e.ndist,e.distcode,0,e.work,M),e.distbits=M.bits,D){i.msg="invalid distances set",e.mode=N;break}if(e.mode=Pt,t===It)break e;case Pt:e.mode=Ht;case Ht:if(h>=6&&_>=258){i.next_out=a,i.avail_out=_,i.next_in=n,i.avail_in=h,e.hold=o,e.bits=l,di(i,u),a=i.next_out,r=i.output,_=i.avail_out,n=i.next_in,s=i.input,h=i.avail_in,o=e.hold,l=e.bits,e.mode===pe&&(e.back=-1);break}for(e.back=0;A=e.lencode[o&(1<>>24,T=A>>>16&255,E=A&65535,!(k<=l);){if(h===0)break e;h--,o+=s[n++]<>K)],k=A>>>24,T=A>>>16&255,E=A&65535,!(K+k<=l);){if(h===0)break e;h--,o+=s[n++]<>>=K,l-=K,e.back+=K}if(o>>>=k,l-=k,e.back+=k,e.length=E,T===0){e.mode=Ts;break}if(T&32){e.back=-1,e.mode=pe;break}if(T&64){i.msg="invalid literal/length code",e.mode=N;break}e.extra=T&15,e.mode=Es;case Es:if(e.extra){for(z=e.extra;l>>=e.extra,l-=e.extra,e.back+=e.extra}e.was=e.length,e.mode=As;case As:for(;A=e.distcode[o&(1<>>24,T=A>>>16&255,E=A&65535,!(k<=l);){if(h===0)break e;h--,o+=s[n++]<>K)],k=A>>>24,T=A>>>16&255,E=A&65535,!(K+k<=l);){if(h===0)break e;h--,o+=s[n++]<>>=K,l-=K,e.back+=K}if(o>>>=k,l-=k,e.back+=k,T&64){i.msg="invalid distance code",e.mode=N;break}e.offset=E,e.extra=T&15,e.mode=Cs;case Cs:if(e.extra){for(z=e.extra;l>>=e.extra,l-=e.extra,e.back+=e.extra}if(e.offset>e.dmax){i.msg="invalid distance too far back",e.mode=N;break}e.mode=Ls;case Ls:if(_===0)break e;if(d=u-_,e.offset>d){if(d=e.offset-d,d>e.whave&&e.sane){i.msg="invalid distance too far back",e.mode=N;break}d>e.wnext?(d-=e.wnext,X=e.wsize-d):X=e.wnext-d,d>e.length&&(d=e.length),F=e.window}else F=r,X=a-e.offset,d=e.length;d>_&&(d=_),_-=d,e.length-=d;do r[a++]=F[X++];while(--d);e.length===0&&(e.mode=Ht);break;case Ts:if(_===0)break e;r[a++]=e.length,_--,e.mode=Ht;break;case bi:if(e.wrap){for(;l<32;){if(h===0)break e;h--,o|=s[n++]<this.chunkSize&&(this.chunkSize=t,this.strm.output=new Uint8Array(this.chunkSize)),this.strm.next_out=0,this.strm.avail_out=t,Ns(this.strm,0)<0)throw new Error("zlib inflate failed");if(this.strm.next_out!=t)throw new Error("Incomplete zlib block");return new Uint8Array(this.strm.output.buffer,0,this.strm.next_out)}reset(){vi(this.strm)}};var wn=4,Os=0,zs=1,vn=2;function We(i){for(var t=i.length;--t>=0;)i[t]=0}var yn=0,js=1,Xn=2,Kn=3,Fn=258,Ei=29,ft=256,ht=ft+1+Ei,Ve=30,Ai=19,qs=2*ht+1,Le=15,yi=16,kn=7,Ci=256,Js=16,$s=17,er=18,ki=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],Nt=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Sn=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],tr=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],En=512,ge=new Array((ht+2)*2);We(ge);var ot=new Array(Ve*2);We(ot);var lt=new Array(En);We(lt);var ct=new Array(Fn-Kn+1);We(ct);var Li=new Array(Ei);We(Li);var Ot=new Array(Ve);We(Ot);function Xi(i,t,e,s,r){this.static_tree=i,this.extra_bits=t,this.extra_base=e,this.elems=s,this.max_length=r,this.has_stree=i&&i.length}var ir,sr,rr;function Ki(i,t){this.dyn_tree=i,this.max_code=0,this.stat_desc=t}function nr(i){return i<256?lt[i]:lt[256+(i>>>7)]}function _t(i,t){i.pending_buf[i.pending++]=t&255,i.pending_buf[i.pending++]=t>>>8&255}function re(i,t,e){i.bi_valid>yi-e?(i.bi_buf|=t<>yi-i.bi_valid,i.bi_valid+=e-yi):(i.bi_buf|=t<>>=1,e<<=1;while(--t>0);return e>>>1}function An(i){i.bi_valid===16?(_t(i,i.bi_buf),i.bi_buf=0,i.bi_valid=0):i.bi_valid>=8&&(i.pending_buf[i.pending++]=i.bi_buf&255,i.bi_buf>>=8,i.bi_valid-=8)}function Cn(i,t){var e=t.dyn_tree,s=t.max_code,r=t.stat_desc.static_tree,n=t.stat_desc.has_stree,a=t.stat_desc.extra_bits,h=t.stat_desc.extra_base,_=t.stat_desc.max_length,o,l,x,u,d,X,F=0;for(u=0;u<=Le;u++)i.bl_count[u]=0;for(e[i.heap[i.heap_max]*2+1]=0,o=i.heap_max+1;o_&&(u=_,F++),e[l*2+1]=u,!(l>s)&&(i.bl_count[u]++,d=0,l>=h&&(d=a[l-h]),X=e[l*2],i.opt_len+=X*(u+d),n&&(i.static_len+=X*(r[l*2+1]+d)));if(F!==0){do{for(u=_-1;i.bl_count[u]===0;)u--;i.bl_count[u]--,i.bl_count[u+1]+=2,i.bl_count[_]--,F-=2}while(F>0);for(u=_;u!==0;u--)for(l=i.bl_count[u];l!==0;)x=i.heap[--o],!(x>s)&&(e[x*2+1]!==u&&(i.opt_len+=(u-e[x*2+1])*e[x*2],e[x*2+1]=u),l--)}}function or(i,t,e){var s=new Array(Le+1),r=0,n,a;for(n=1;n<=Le;n++)s[n]=r=r+e[n-1]<<1;for(a=0;a<=t;a++){var h=i[a*2+1];h!==0&&(i[a*2]=ar(s[h]++,h))}}function Ln(){var i,t,e,s,r,n=new Array(Le+1);for(e=0,s=0;s>=7;s8?_t(i,i.bi_buf):i.bi_valid>0&&(i.pending_buf[i.pending++]=i.bi_buf),i.bi_buf=0,i.bi_valid=0}function Tn(i,t,e,s){lr(i),s&&(_t(i,e),_t(i,~e)),ae(i.pending_buf,i.window,t,e,i.pending),i.pending+=e}function Gs(i,t,e,s){var r=t*2,n=e*2;return i[r]>1;a>=1;a--)Fi(i,e,a);o=n;do a=i.heap[1],i.heap[1]=i.heap[i.heap_len--],Fi(i,e,1),h=i.heap[1],i.heap[--i.heap_max]=a,i.heap[--i.heap_max]=h,e[o*2]=e[a*2]+e[h*2],i.depth[o]=(i.depth[a]>=i.depth[h]?i.depth[a]:i.depth[h])+1,e[a*2+1]=e[h*2+1]=o,i.heap[1]=o++,Fi(i,e,1);while(i.heap_len>=2);i.heap[--i.heap_max]=i.heap[1],Cn(i,t),or(e,_,i.bl_count)}function Ws(i,t,e){var s,r=-1,n,a=t[0*2+1],h=0,_=7,o=4;for(a===0&&(_=138,o=3),t[(e+1)*2+1]=65535,s=0;s<=e;s++)n=a,a=t[(s+1)*2+1],!(++h<_&&n===a)&&(h=3&&i.bl_tree[tr[t]*2+1]===0;t--);return i.opt_len+=3*(t+1)+5+5+4,t}function Rn(i,t,e,s){var r;for(re(i,t-257,5),re(i,e-1,5),re(i,s-4,4),r=0;r>>=1)if(t&1&&i.dyn_ltree[e*2]!==0)return Os;if(i.dyn_ltree[9*2]!==0||i.dyn_ltree[10*2]!==0||i.dyn_ltree[13*2]!==0)return zs;for(e=32;e0?(i.strm.data_type===vn&&(i.strm.data_type=Bn(i)),Si(i,i.l_desc),Si(i,i.d_desc),a=Mn(i),r=i.opt_len+3+7>>>3,n=i.static_len+3+7>>>3,n<=r&&(r=n)):r=n=e+5,e+4<=r&&t!==-1?Ti(i,t,e,s):i.strategy===wn||n===r?(re(i,(js<<1)+(s?1:0),3),Vs(i,ge,ot)):(re(i,(Xn<<1)+(s?1:0),3),Rn(i,i.l_desc.max_code+1,i.d_desc.max_code+1,a+1),Vs(i,i.dyn_ltree,i.dyn_dtree)),hr(i),s&&lr(i)}function be(i,t,e){return i.pending_buf[i.d_buf+i.last_lit*2]=t>>>8&255,i.pending_buf[i.d_buf+i.last_lit*2+1]=t&255,i.pending_buf[i.l_buf+i.last_lit]=e&255,i.last_lit++,t===0?i.dyn_ltree[e*2]++:(i.matches++,t--,i.dyn_ltree[(ct[e]+ft+1)*2]++,i.dyn_dtree[nr(t)*2]++),i.last_lit===i.lit_bufsize-1}var ur={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"};var Be=0,Qn=1,Vt=3,ye=4,dr=5,Te=0,xr=1,qe=-2;var Mi=-5,Pi=-1,In=1,zt=2,Pn=3,Hn=4,Un=0,Nn=2,Wt=8,On=9,zn=15,Gn=8,Vn=29,Wn=256,Ii=Wn+1+Vn,Zn=30,Yn=19,jn=2*Ii+1,qn=15,P=3,ve=258,ce=ve+P+1,Jn=32,br=42,pr=69,Ri=73,Bi=91,Di=103,dt=113,Gt=666,Y=1,xt=2,Re=3,Je=4,$n=3;function Ye(i,t){return i.msg=ur[t],t}function gr(i){return(i<<1)-(i>4?9:0)}function Me(i){for(var t=i.length;--t>=0;)i[t]=0}function we(i){var t=i.state,e=t.pending;e>i.avail_out&&(e=i.avail_out),e!==0&&(ae(i.output,t.pending_buf,t.pending_out,e,i.next_out),i.next_out+=e,t.pending_out+=e,i.total_out+=e,i.avail_out-=e,t.pending-=e,t.pending===0&&(t.pending_out=0))}function j(i,t){fr(i,i.block_start>=0?i.block_start:-1,i.strstart-i.block_start,t),i.block_start=i.strstart,we(i.strm)}function Q(i,t){i.pending_buf[i.pending++]=t}function ut(i,t){i.pending_buf[i.pending++]=t>>>8&255,i.pending_buf[i.pending++]=t&255}function ea(i,t,e,s){var r=i.avail_in;return r>s&&(r=s),r===0?0:(i.avail_in-=r,ae(t,i.input,i.next_in,r,e),i.state.wrap===1?i.adler=ze(i.adler,t,r,e):i.state.wrap===2&&(i.adler=G(i.adler,t,r,e)),i.next_in+=r,i.total_in+=r,r)}function mr(i,t){var e=i.max_chain_length,s=i.strstart,r,n,a=i.prev_length,h=i.nice_match,_=i.strstart>i.w_size-ce?i.strstart-(i.w_size-ce):0,o=i.window,l=i.w_mask,x=i.prev,u=i.strstart+ve,d=o[s+a-1],X=o[s+a];i.prev_length>=i.good_match&&(e>>=2),h>i.lookahead&&(h=i.lookahead);do if(r=t,!(o[r+a]!==X||o[r+a-1]!==d||o[r]!==o[s]||o[++r]!==o[s+1])){s+=2,r++;do;while(o[++s]===o[++r]&&o[++s]===o[++r]&&o[++s]===o[++r]&&o[++s]===o[++r]&&o[++s]===o[++r]&&o[++s]===o[++r]&&o[++s]===o[++r]&&o[++s]===o[++r]&&sa){if(i.match_start=t,a=n,n>=h)break;d=o[s+a-1],X=o[s+a]}}while((t=x[t&l])>_&&--e!==0);return a<=i.lookahead?a:i.lookahead}function pt(i){var t=i.w_size,e,s,r,n,a;do{if(n=i.window_size-i.lookahead-i.strstart,i.strstart>=t+(t-ce)){ae(i.window,i.window,t,t,0),i.match_start-=t,i.strstart-=t,i.block_start-=t,s=i.hash_size,e=s;do r=i.head[--e],i.head[e]=r>=t?r-t:0;while(--s);s=t,e=s;do r=i.prev[--e],i.prev[e]=r>=t?r-t:0;while(--s);n+=t}if(i.strm.avail_in===0)break;if(s=ea(i.strm,i.window,i.strstart+i.lookahead,n),i.lookahead+=s,i.lookahead+i.insert>=P)for(a=i.strstart-i.insert,i.ins_h=i.window[a],i.ins_h=(i.ins_h<i.pending_buf_size-5&&(e=i.pending_buf_size-5);;){if(i.lookahead<=1){if(pt(i),i.lookahead===0&&t===Be)return Y;if(i.lookahead===0)break}i.strstart+=i.lookahead,i.lookahead=0;var s=i.block_start+e;if((i.strstart===0||i.strstart>=s)&&(i.lookahead=i.strstart-s,i.strstart=s,j(i,!1),i.strm.avail_out===0)||i.strstart-i.block_start>=i.w_size-ce&&(j(i,!1),i.strm.avail_out===0))return Y}return i.insert=0,t===ye?(j(i,!0),i.strm.avail_out===0?Re:Je):(i.strstart>i.block_start&&(j(i,!1),i.strm.avail_out===0),Y)}function Qi(i,t){for(var e,s;;){if(i.lookahead=P&&(i.ins_h=(i.ins_h<=P)if(s=be(i,i.strstart-i.match_start,i.match_length-P),i.lookahead-=i.match_length,i.match_length<=i.max_lazy_match&&i.lookahead>=P){i.match_length--;do i.strstart++,i.ins_h=(i.ins_h<=P&&(i.ins_h=(i.ins_h<4096)&&(i.match_length=P-1)),i.prev_length>=P&&i.match_length<=i.prev_length){r=i.strstart+i.lookahead-P,s=be(i,i.strstart-1-i.prev_match,i.prev_length-P),i.lookahead-=i.prev_length-1,i.prev_length-=2;do++i.strstart<=r&&(i.ins_h=(i.ins_h<=P&&i.strstart>0&&(r=i.strstart-1,s=a[r],s===a[++r]&&s===a[++r]&&s===a[++r])){n=i.strstart+ve;do;while(s===a[++r]&&s===a[++r]&&s===a[++r]&&s===a[++r]&&s===a[++r]&&s===a[++r]&&s===a[++r]&&s===a[++r]&&ri.lookahead&&(i.match_length=i.lookahead)}if(i.match_length>=P?(e=be(i,1,i.match_length-P),i.lookahead-=i.match_length,i.strstart+=i.match_length,i.match_length=0):(e=be(i,0,i.window[i.strstart]),i.lookahead--,i.strstart++),e&&(j(i,!1),i.strm.avail_out===0))return Y}return i.insert=0,t===ye?(j(i,!0),i.strm.avail_out===0?Re:Je):i.last_lit&&(j(i,!1),i.strm.avail_out===0)?Y:xt}function sa(i,t){for(var e;;){if(i.lookahead===0&&(pt(i),i.lookahead===0)){if(t===Be)return Y;break}if(i.match_length=0,e=be(i,0,i.window[i.strstart]),i.lookahead--,i.strstart++,e&&(j(i,!1),i.strm.avail_out===0))return Y}return i.insert=0,t===ye?(j(i,!0),i.strm.avail_out===0?Re:Je):i.last_lit&&(j(i,!1),i.strm.avail_out===0)?Y:xt}function ue(i,t,e,s,r){this.good_length=i,this.max_lazy=t,this.nice_length=e,this.max_chain=s,this.func=r}var je;je=[new ue(0,0,0,0,ta),new ue(4,4,8,4,Qi),new ue(4,5,16,8,Qi),new ue(4,6,32,32,Qi),new ue(4,4,16,16,Ze),new ue(8,16,32,32,Ze),new ue(8,16,128,128,Ze),new ue(8,32,128,256,Ze),new ue(32,128,258,1024,Ze),new ue(32,258,258,4096,Ze)];function ra(i){i.window_size=2*i.w_size,Me(i.head),i.max_lazy_match=je[i.level].max_lazy,i.good_match=je[i.level].good_length,i.nice_match=je[i.level].nice_length,i.max_chain_length=je[i.level].max_chain,i.strstart=0,i.block_start=0,i.lookahead=0,i.insert=0,i.match_length=i.prev_length=P-1,i.match_available=0,i.ins_h=0}function na(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=Wt,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new ie(jn*2),this.dyn_dtree=new ie((2*Zn+1)*2),this.bl_tree=new ie((2*Yn+1)*2),Me(this.dyn_ltree),Me(this.dyn_dtree),Me(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new ie(qn+1),this.heap=new ie(2*Ii+1),Me(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new ie(2*Ii+1),Me(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function aa(i){var t;return!i||!i.state?Ye(i,qe):(i.total_in=i.total_out=0,i.data_type=Nn,t=i.state,t.pending=0,t.pending_out=0,t.wrap<0&&(t.wrap=-t.wrap),t.status=t.wrap?br:dt,i.adler=t.wrap===2?0:1,t.last_flush=Be,cr(t),Te)}function oa(i){var t=aa(i);return t===Te&&ra(i.state),t}function ha(i,t,e,s,r,n){if(!i)return qe;var a=1;if(t===Pi&&(t=6),s<0?(a=0,s=-s):s>15&&(a=2,s-=16),r<1||r>On||e!==Wt||s<8||s>15||t<0||t>9||n<0||n>Hn)return Ye(i,qe);s===8&&(s=9);var h=new na;return i.state=h,h.strm=i,h.wrap=a,h.gzhead=null,h.w_bits=s,h.w_size=1<dr||t<0)return i?Ye(i,qe):qe;if(s=i.state,!i.output||!i.input&&i.avail_in!==0||s.status===Gt&&t!==ye)return Ye(i,i.avail_out===0?Mi:qe);if(s.strm=i,e=s.last_flush,s.last_flush=t,s.status===br)if(s.wrap===2)i.adler=0,Q(s,31),Q(s,139),Q(s,8),s.gzhead?(Q(s,(s.gzhead.text?1:0)+(s.gzhead.hcrc?2:0)+(s.gzhead.extra?4:0)+(s.gzhead.name?8:0)+(s.gzhead.comment?16:0)),Q(s,s.gzhead.time&255),Q(s,s.gzhead.time>>8&255),Q(s,s.gzhead.time>>16&255),Q(s,s.gzhead.time>>24&255),Q(s,s.level===9?2:s.strategy>=zt||s.level<2?4:0),Q(s,s.gzhead.os&255),s.gzhead.extra&&s.gzhead.extra.length&&(Q(s,s.gzhead.extra.length&255),Q(s,s.gzhead.extra.length>>8&255)),s.gzhead.hcrc&&(i.adler=G(i.adler,s.pending_buf,s.pending,0)),s.gzindex=0,s.status=pr):(Q(s,0),Q(s,0),Q(s,0),Q(s,0),Q(s,0),Q(s,s.level===9?2:s.strategy>=zt||s.level<2?4:0),Q(s,$n),s.status=dt);else{var a=Wt+(s.w_bits-8<<4)<<8,h=-1;s.strategy>=zt||s.level<2?h=0:s.level<6?h=1:s.level===6?h=2:h=3,a|=h<<6,s.strstart!==0&&(a|=Jn),a+=31-a%31,s.status=dt,ut(s,a),s.strstart!==0&&(ut(s,i.adler>>>16),ut(s,i.adler&65535)),i.adler=1}if(s.status===pr)if(s.gzhead.extra){for(r=s.pending;s.gzindex<(s.gzhead.extra.length&65535)&&!(s.pending===s.pending_buf_size&&(s.gzhead.hcrc&&s.pending>r&&(i.adler=G(i.adler,s.pending_buf,s.pending-r,r)),we(i),r=s.pending,s.pending===s.pending_buf_size));)Q(s,s.gzhead.extra[s.gzindex]&255),s.gzindex++;s.gzhead.hcrc&&s.pending>r&&(i.adler=G(i.adler,s.pending_buf,s.pending-r,r)),s.gzindex===s.gzhead.extra.length&&(s.gzindex=0,s.status=Ri)}else s.status=Ri;if(s.status===Ri)if(s.gzhead.name){r=s.pending;do{if(s.pending===s.pending_buf_size&&(s.gzhead.hcrc&&s.pending>r&&(i.adler=G(i.adler,s.pending_buf,s.pending-r,r)),we(i),r=s.pending,s.pending===s.pending_buf_size)){n=1;break}s.gzindexr&&(i.adler=G(i.adler,s.pending_buf,s.pending-r,r)),n===0&&(s.gzindex=0,s.status=Bi)}else s.status=Bi;if(s.status===Bi)if(s.gzhead.comment){r=s.pending;do{if(s.pending===s.pending_buf_size&&(s.gzhead.hcrc&&s.pending>r&&(i.adler=G(i.adler,s.pending_buf,s.pending-r,r)),we(i),r=s.pending,s.pending===s.pending_buf_size)){n=1;break}s.gzindexr&&(i.adler=G(i.adler,s.pending_buf,s.pending-r,r)),n===0&&(s.status=Di)}else s.status=Di;if(s.status===Di&&(s.gzhead.hcrc?(s.pending+2>s.pending_buf_size&&we(i),s.pending+2<=s.pending_buf_size&&(Q(s,i.adler&255),Q(s,i.adler>>8&255),i.adler=0,s.status=dt)):s.status=dt),s.pending!==0){if(we(i),i.avail_out===0)return s.last_flush=-1,Te}else if(i.avail_in===0&&gr(t)<=gr(e)&&t!==ye)return Ye(i,Mi);if(s.status===Gt&&i.avail_in!==0)return Ye(i,Mi);if(i.avail_in!==0||s.lookahead!==0||t!==Be&&s.status!==Gt){var _=s.strategy===zt?sa(s,t):s.strategy===Pn?ia(s,t):je[s.level].func(s,t);if((_===Re||_===Je)&&(s.status=Gt),_===Y||_===Re)return i.avail_out===0&&(s.last_flush=-1),Te;if(_===xt&&(t===Qn?_r(s):t!==dr&&(Ti(s,0,0,!1),t===Vt&&(Me(s.head),s.lookahead===0&&(s.strstart=0,s.block_start=0,s.insert=0))),we(i),i.avail_out===0))return s.last_flush=-1,Te}return t!==ye?Te:s.wrap<=0?xr:(s.wrap===2?(Q(s,i.adler&255),Q(s,i.adler>>8&255),Q(s,i.adler>>16&255),Q(s,i.adler>>24&255),Q(s,i.total_in&255),Q(s,i.total_in>>8&255),Q(s,i.total_in>>16&255),Q(s,i.total_in>>24&255)):(ut(s,i.adler>>>16),ut(s,i.adler&65535)),we(i),s.wrap>0&&(s.wrap=-s.wrap),s.pending!==0?Te:xr)}var gt=class{constructor(){this.strm=new at,this.chunkSize=1024*10*10,this.outputBuffer=new Uint8Array(this.chunkSize),wr(this.strm,Pi)}deflate(t){this.strm.input=t,this.strm.avail_in=this.strm.input.length,this.strm.next_in=0,this.strm.output=this.outputBuffer,this.strm.avail_out=this.chunkSize,this.strm.next_out=0;let e=Hi(this.strm,Vt),s=new Uint8Array(this.strm.output.buffer,0,this.strm.next_out);if(e<0)throw new Error("zlib deflate failed");if(this.strm.avail_in>0){let r=[s],n=s.length;do{if(this.strm.output=new Uint8Array(this.chunkSize),this.strm.next_out=0,this.strm.avail_out=this.chunkSize,e=Hi(this.strm,Vt),e<0)throw new Error("zlib deflate failed");let _=new Uint8Array(this.strm.output.buffer,0,this.strm.next_out);n+=_.length,r.push(_)}while(this.strm.avail_in>0);let a=new Uint8Array(n),h=0;for(let _=0;_=32&&i<=255)return i;let t=la[i];return t!==void 0?t:16777216|i}};var Ui={8:"Backspace",9:"Tab",10:"NumpadClear",13:"Enter",16:"ShiftLeft",17:"ControlLeft",18:"AltLeft",19:"Pause",20:"CapsLock",21:"Lang1",25:"Lang2",27:"Escape",28:"Convert",29:"NonConvert",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",41:"Select",44:"PrintScreen",45:"Insert",46:"Delete",47:"Help",48:"Digit0",49:"Digit1",50:"Digit2",51:"Digit3",52:"Digit4",53:"Digit5",54:"Digit6",55:"Digit7",56:"Digit8",57:"Digit9",91:"MetaLeft",92:"MetaRight",93:"ContextMenu",95:"Sleep",96:"Numpad0",97:"Numpad1",98:"Numpad2",99:"Numpad3",100:"Numpad4",101:"Numpad5",102:"Numpad6",103:"Numpad7",104:"Numpad8",105:"Numpad9",106:"NumpadMultiply",107:"NumpadAdd",108:"NumpadDecimal",109:"NumpadSubtract",110:"NumpadDecimal",111:"NumpadDivide",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",124:"F13",125:"F14",126:"F15",127:"F16",128:"F17",129:"F18",130:"F19",131:"F20",132:"F21",133:"F22",134:"F23",135:"F24",144:"NumLock",145:"ScrollLock",166:"BrowserBack",167:"BrowserForward",168:"BrowserRefresh",169:"BrowserStop",170:"BrowserSearch",171:"BrowserFavorites",172:"BrowserHome",173:"AudioVolumeMute",174:"AudioVolumeDown",175:"AudioVolumeUp",176:"MediaTrackNext",177:"MediaTrackPrevious",178:"MediaStop",179:"MediaPlayPause",180:"LaunchMail",181:"MediaSelect",182:"LaunchApp1",183:"LaunchApp2",225:"AltRight"};var Ni={Backspace:"Backspace",AltLeft:"Alt",AltRight:"Alt",CapsLock:"CapsLock",ContextMenu:"ContextMenu",ControlLeft:"Control",ControlRight:"Control",Enter:"Enter",MetaLeft:"Meta",MetaRight:"Meta",ShiftLeft:"Shift",ShiftRight:"Shift",Tab:"Tab",Delete:"Delete",End:"End",Help:"Help",Home:"Home",Insert:"Insert",PageDown:"PageDown",PageUp:"PageUp",ArrowDown:"ArrowDown",ArrowLeft:"ArrowLeft",ArrowRight:"ArrowRight",ArrowUp:"ArrowUp",NumLock:"NumLock",NumpadBackspace:"Backspace",NumpadClear:"Clear",Escape:"Escape",F1:"F1",F2:"F2",F3:"F3",F4:"F4",F5:"F5",F6:"F6",F7:"F7",F8:"F8",F9:"F9",F10:"F10",F11:"F11",F12:"F12",F13:"F13",F14:"F14",F15:"F15",F16:"F16",F17:"F17",F18:"F18",F19:"F19",F20:"F20",F21:"F21",F22:"F22",F23:"F23",F24:"F24",F25:"F25",F26:"F26",F27:"F27",F28:"F28",F29:"F29",F30:"F30",F31:"F31",F32:"F32",F33:"F33",F34:"F34",F35:"F35",PrintScreen:"PrintScreen",ScrollLock:"ScrollLock",Pause:"Pause",BrowserBack:"BrowserBack",BrowserFavorites:"BrowserFavorites",BrowserForward:"BrowserForward",BrowserHome:"BrowserHome",BrowserRefresh:"BrowserRefresh",BrowserSearch:"BrowserSearch",BrowserStop:"BrowserStop",Eject:"Eject",LaunchApp1:"LaunchMyComputer",LaunchApp2:"LaunchCalendar",LaunchMail:"LaunchMail",MediaPlayPause:"MediaPlay",MediaStop:"MediaStop",MediaTrackNext:"MediaTrackNext",MediaTrackPrevious:"MediaTrackPrevious",Power:"Power",Sleep:"Sleep",AudioVolumeDown:"AudioVolumeDown",AudioVolumeMute:"AudioVolumeMute",AudioVolumeUp:"AudioVolumeUp",WakeUp:"WakeUp"};var De={};function w(i,t){if(t===void 0)throw new Error('Undefined keysym for key "'+i+'"');if(i in De)throw new Error('Duplicate entry for key "'+i+'"');De[i]=[t,t,t,t]}function Zt(i,t,e){if(t===void 0)throw new Error('Undefined keysym for key "'+i+'"');if(e===void 0)throw new Error('Undefined keysym for key "'+i+'"');if(i in De)throw new Error('Duplicate entry for key "'+i+'"');De[i]=[t,t,e,t]}function H(i,t,e){if(t===void 0)throw new Error('Undefined keysym for key "'+i+'"');if(e===void 0)throw new Error('Undefined keysym for key "'+i+'"');if(i in De)throw new Error('Duplicate entry for key "'+i+'"');De[i]=[t,t,t,e]}Zt("Alt",f.XK_Alt_L,f.XK_Alt_R);w("AltGraph",f.XK_ISO_Level3_Shift);w("CapsLock",f.XK_Caps_Lock);Zt("Control",f.XK_Control_L,f.XK_Control_R);Zt("Meta",f.XK_Super_L,f.XK_Super_R);w("NumLock",f.XK_Num_Lock);w("ScrollLock",f.XK_Scroll_Lock);Zt("Shift",f.XK_Shift_L,f.XK_Shift_R);H("Enter",f.XK_Return,f.XK_KP_Enter);w("Tab",f.XK_Tab);H(" ",f.XK_space,f.XK_KP_Space);H("ArrowDown",f.XK_Down,f.XK_KP_Down);H("ArrowLeft",f.XK_Left,f.XK_KP_Left);H("ArrowRight",f.XK_Right,f.XK_KP_Right);H("ArrowUp",f.XK_Up,f.XK_KP_Up);H("End",f.XK_End,f.XK_KP_End);H("Home",f.XK_Home,f.XK_KP_Home);H("PageDown",f.XK_Next,f.XK_KP_Next);H("PageUp",f.XK_Prior,f.XK_KP_Prior);w("Backspace",f.XK_BackSpace);H("Clear",f.XK_Clear,f.XK_KP_Begin);w("Copy",f.XF86XK_Copy);w("Cut",f.XF86XK_Cut);H("Delete",f.XK_Delete,f.XK_KP_Delete);H("Insert",f.XK_Insert,f.XK_KP_Insert);w("Paste",f.XF86XK_Paste);w("Redo",f.XK_Redo);w("Undo",f.XK_Undo);w("Cancel",f.XK_Cancel);w("ContextMenu",f.XK_Menu);w("Escape",f.XK_Escape);w("Execute",f.XK_Execute);w("Find",f.XK_Find);w("Help",f.XK_Help);w("Pause",f.XK_Pause);w("Select",f.XK_Select);w("ZoomIn",f.XF86XK_ZoomIn);w("ZoomOut",f.XF86XK_ZoomOut);w("BrightnessDown",f.XF86XK_MonBrightnessDown);w("BrightnessUp",f.XF86XK_MonBrightnessUp);w("Eject",f.XF86XK_Eject);w("LogOff",f.XF86XK_LogOff);w("Power",f.XF86XK_PowerOff);w("PowerOff",f.XF86XK_PowerDown);w("PrintScreen",f.XK_Print);w("Hibernate",f.XF86XK_Hibernate);w("Standby",f.XF86XK_Standby);w("WakeUp",f.XF86XK_WakeUp);w("AllCandidates",f.XK_MultipleCandidate);w("Alphanumeric",f.XK_Eisu_toggle);w("CodeInput",f.XK_Codeinput);w("Compose",f.XK_Multi_key);w("Convert",f.XK_Henkan);w("GroupFirst",f.XK_ISO_First_Group);w("GroupLast",f.XK_ISO_Last_Group);w("GroupNext",f.XK_ISO_Next_Group);w("GroupPrevious",f.XK_ISO_Prev_Group);w("NonConvert",f.XK_Muhenkan);w("PreviousCandidate",f.XK_PreviousCandidate);w("SingleCandidate",f.XK_SingleCandidate);w("HangulMode",f.XK_Hangul);w("HanjaMode",f.XK_Hangul_Hanja);w("JunjaMode",f.XK_Hangul_Jeonja);w("Eisu",f.XK_Eisu_toggle);w("Hankaku",f.XK_Hankaku);w("Hiragana",f.XK_Hiragana);w("HiraganaKatakana",f.XK_Hiragana_Katakana);w("KanaMode",f.XK_Kana_Shift);w("KanjiMode",f.XK_Kanji);w("Katakana",f.XK_Katakana);w("Romaji",f.XK_Romaji);w("Zenkaku",f.XK_Zenkaku);w("ZenkakuHankaku",f.XK_Zenkaku_Hankaku);w("F1",f.XK_F1);w("F2",f.XK_F2);w("F3",f.XK_F3);w("F4",f.XK_F4);w("F5",f.XK_F5);w("F6",f.XK_F6);w("F7",f.XK_F7);w("F8",f.XK_F8);w("F9",f.XK_F9);w("F10",f.XK_F10);w("F11",f.XK_F11);w("F12",f.XK_F12);w("F13",f.XK_F13);w("F14",f.XK_F14);w("F15",f.XK_F15);w("F16",f.XK_F16);w("F17",f.XK_F17);w("F18",f.XK_F18);w("F19",f.XK_F19);w("F20",f.XK_F20);w("F21",f.XK_F21);w("F22",f.XK_F22);w("F23",f.XK_F23);w("F24",f.XK_F24);w("F25",f.XK_F25);w("F26",f.XK_F26);w("F27",f.XK_F27);w("F28",f.XK_F28);w("F29",f.XK_F29);w("F30",f.XK_F30);w("F31",f.XK_F31);w("F32",f.XK_F32);w("F33",f.XK_F33);w("F34",f.XK_F34);w("F35",f.XK_F35);w("Close",f.XF86XK_Close);w("MailForward",f.XF86XK_MailForward);w("MailReply",f.XF86XK_Reply);w("MailSend",f.XF86XK_Send);w("MediaFastForward",f.XF86XK_AudioForward);w("MediaPause",f.XF86XK_AudioPause);w("MediaPlay",f.XF86XK_AudioPlay);w("MediaRecord",f.XF86XK_AudioRecord);w("MediaRewind",f.XF86XK_AudioRewind);w("MediaStop",f.XF86XK_AudioStop);w("MediaTrackNext",f.XF86XK_AudioNext);w("MediaTrackPrevious",f.XF86XK_AudioPrev);w("New",f.XF86XK_New);w("Open",f.XF86XK_Open);w("Print",f.XK_Print);w("Save",f.XF86XK_Save);w("SpellCheck",f.XF86XK_Spell);w("AudioVolumeDown",f.XF86XK_AudioLowerVolume);w("AudioVolumeUp",f.XF86XK_AudioRaiseVolume);w("AudioVolumeMute",f.XF86XK_AudioMute);w("MicrophoneVolumeMute",f.XF86XK_AudioMicMute);w("LaunchApplication1",f.XF86XK_MyComputer);w("LaunchApplication2",f.XF86XK_Calculator);w("LaunchCalendar",f.XF86XK_Calendar);w("LaunchMail",f.XF86XK_Mail);w("LaunchMediaPlayer",f.XF86XK_AudioMedia);w("LaunchMusicPlayer",f.XF86XK_Music);w("LaunchPhone",f.XF86XK_Phone);w("LaunchScreenSaver",f.XF86XK_ScreenSaver);w("LaunchSpreadsheet",f.XF86XK_Excel);w("LaunchWebBrowser",f.XF86XK_WWW);w("LaunchWebCam",f.XF86XK_WebCam);w("LaunchWordProcessor",f.XF86XK_Word);w("BrowserBack",f.XF86XK_Back);w("BrowserFavorites",f.XF86XK_Favorites);w("BrowserForward",f.XF86XK_Forward);w("BrowserHome",f.XF86XK_HomePage);w("BrowserRefresh",f.XF86XK_Refresh);w("BrowserSearch",f.XF86XK_Search);w("BrowserStop",f.XF86XK_Stop);w("Dimmer",f.XF86XK_BrightnessAdjust);w("MediaAudioTrack",f.XF86XK_AudioCycleTrack);w("RandomToggle",f.XF86XK_AudioRandomPlay);w("SplitScreenToggle",f.XF86XK_SplitScreen);w("Subtitle",f.XF86XK_Subtitle);w("VideoModeNext",f.XF86XK_Next_VMode);H("=",f.XK_equal,f.XK_KP_Equal);H("+",f.XK_plus,f.XK_KP_Add);H("-",f.XK_minus,f.XK_KP_Subtract);H("*",f.XK_asterisk,f.XK_KP_Multiply);H("/",f.XK_slash,f.XK_KP_Divide);H(".",f.XK_period,f.XK_KP_Decimal);H(",",f.XK_comma,f.XK_KP_Separator);H("0",f.XK_0,f.XK_KP_0);H("1",f.XK_1,f.XK_KP_1);H("2",f.XK_2,f.XK_KP_2);H("3",f.XK_3,f.XK_KP_3);H("4",f.XK_4,f.XK_KP_4);H("5",f.XK_5,f.XK_KP_5);H("6",f.XK_6,f.XK_KP_6);H("7",f.XK_7,f.XK_KP_7);H("8",f.XK_8,f.XK_KP_8);H("9",f.XK_9,f.XK_KP_9);var Oi=De;function $e(i){if(i.code){switch(i.code){case"OSLeft":return"MetaLeft";case"OSRight":return"MetaRight"}return i.code}if(i.keyCode in Ui){let t=Ui[i.keyCode];if(me()&&t==="ContextMenu"&&(t="MetaRight"),i.location===2)switch(t){case"ShiftLeft":return"ShiftRight";case"ControlLeft":return"ControlRight";case"AltLeft":return"AltRight"}if(i.location===3)switch(t){case"Delete":return"NumpadDecimal";case"Insert":return"Numpad0";case"End":return"Numpad1";case"ArrowDown":return"Numpad2";case"PageDown":return"Numpad3";case"ArrowLeft":return"Numpad4";case"ArrowRight":return"Numpad6";case"Home":return"Numpad7";case"ArrowUp":return"Numpad8";case"PageUp":return"Numpad9";case"Enter":return"NumpadEnter"}return t}return"Unidentified"}function ca(i){if(i.key!==void 0&&i.key!=="Unidentified"){switch(i.key){case"OS":return"Meta";case"LaunchMyComputer":return"LaunchApplication1";case"LaunchCalculator":return"LaunchApplication2"}switch(i.key){case"UIKeyInputUpArrow":return"ArrowUp";case"UIKeyInputDownArrow":return"ArrowDown";case"UIKeyInputLeftArrow":return"ArrowLeft";case"UIKeyInputRightArrow":return"ArrowRight";case"UIKeyInputEscape":return"Escape"}return i.key==="\0"&&i.code==="NumpadDecimal"?"Delete":i.key}let t=$e(i);return t in Ni?Ni[t]:i.charCode?String.fromCharCode(i.charCode):"Unidentified"}function yr(i){let t=ca(i);if(t==="Unidentified")return null;if(t in Oi){let s=i.location;if(t==="Meta"&&s===0&&(s=2),t==="Clear"&&s===3&&$e(i)==="NumLock"&&(s=0),(s===void 0||s>3)&&(s=0),t==="Meta"){let r=$e(i);if(r==="AltLeft")return f.XK_Meta_L;if(r==="AltRight")return f.XK_Meta_R}if(t==="Clear"&&$e(i)==="NumLock")return f.XK_Num_Lock;if(He())switch(t){case"Zenkaku":case"Hankaku":return f.XK_Zenkaku_Hankaku;case"Romaji":case"KanaMode":return f.XK_Romaji}return Oi[t][s]}if(t.length!==1)return null;let e=t.charCodeAt();return e?vr.lookup(e):null}var bt=class{constructor(t){this._target=t||null,this._keyDownList={},this._altGrArmed=!1,this._eventHandlers={keyup:this._handleKeyUp.bind(this),keydown:this._handleKeyDown.bind(this),blur:this._allKeysUp.bind(this)},this.onkeyevent=()=>{}}_sendKeyEvent(t,e,s,r=null,n=null){if(s)this._keyDownList[e]=t;else{if(!(e in this._keyDownList))return;delete this._keyDownList[e]}L("onkeyevent "+(s?"down":"up")+", keysym: "+t,", code: "+e+", numlock: "+r+", capslock: "+n),this.onkeyevent(t,e,s,r,n)}_getKeyCode(t){let e=$e(t);if(e!=="Unidentified")return e;if(t.keyCode&&t.keyCode!==229)return"Platform"+t.keyCode;if(t.keyIdentifier){if(t.keyIdentifier.substr(0,2)!=="U+")return t.keyIdentifier;let s=parseInt(t.keyIdentifier.substr(2),16);return"Platform"+String.fromCharCode(s).toUpperCase().charCodeAt()}return"Unidentified"}_handleKeyDown(t){let e=this._getKeyCode(t),s=yr(t),r=t.getModifierState("NumLock"),n=t.getModifierState("CapsLock");if((me()||Ue())&&(r=null),this._altGrArmed&&(this._altGrArmed=!1,clearTimeout(this._altGrTimeout),e==="AltRight"&&t.timeStamp-this._altGrCtrlTime<50?s=f.XK_ISO_Level3_Shift:this._sendKeyEvent(f.XK_Control_L,"ControlLeft",!0,r,n)),e==="Unidentified"){s&&(this._sendKeyEvent(s,e,!0,r,n),this._sendKeyEvent(s,e,!1,r,n)),Se(t);return}if(me()||Ue())switch(s){case f.XK_Super_L:s=f.XK_Alt_L;break;case f.XK_Super_R:s=f.XK_Super_L;break;case f.XK_Alt_L:s=f.XK_Mode_switch;break;case f.XK_Alt_R:s=f.XK_ISO_Level3_Shift;break}if(e in this._keyDownList&&(s=this._keyDownList[e]),(me()||Ue())&&t.metaKey&&e!=="MetaLeft"&&e!=="MetaRight"){this._sendKeyEvent(s,e,!0,r,n),this._sendKeyEvent(s,e,!1,r,n),Se(t);return}if((me()||Ue())&&e==="CapsLock"){this._sendKeyEvent(f.XK_Caps_Lock,"CapsLock",!0,r,n),this._sendKeyEvent(f.XK_Caps_Lock,"CapsLock",!1,r,n),Se(t);return}let a=[f.XK_Zenkaku_Hankaku,f.XK_Eisu_toggle,f.XK_Katakana,f.XK_Hiragana,f.XK_Romaji];if(He()&&a.includes(s)){this._sendKeyEvent(s,e,!0,r,n),this._sendKeyEvent(s,e,!1,r,n),Se(t);return}if(Se(t),e==="ControlLeft"&&He()&&!("ControlLeft"in this._keyDownList)){this._altGrArmed=!0,this._altGrTimeout=setTimeout(this._interruptAltGrSequence.bind(this),100),this._altGrCtrlTime=t.timeStamp;return}this._sendKeyEvent(s,e,!0,r,n)}_handleKeyUp(t){Se(t);let e=this._getKeyCode(t);if(this._interruptAltGrSequence(),(me()||Ue())&&e==="CapsLock"){this._sendKeyEvent(f.XK_Caps_Lock,"CapsLock",!0),this._sendKeyEvent(f.XK_Caps_Lock,"CapsLock",!1);return}this._sendKeyEvent(this._keyDownList[e],e,!1),He()&&(e==="ShiftLeft"||e==="ShiftRight")&&("ShiftRight"in this._keyDownList&&this._sendKeyEvent(this._keyDownList.ShiftRight,"ShiftRight",!1),"ShiftLeft"in this._keyDownList&&this._sendKeyEvent(this._keyDownList.ShiftLeft,"ShiftLeft",!1))}_interruptAltGrSequence(){this._altGrArmed&&(this._altGrArmed=!1,clearTimeout(this._altGrTimeout),this._sendKeyEvent(f.XK_Control_L,"ControlLeft",!0))}_allKeysUp(){L(">> Keyboard.allKeysUp"),this._interruptAltGrSequence();for(let t in this._keyDownList)this._sendKeyEvent(this._keyDownList[t],t,!1);L("<< Keyboard.allKeysUp")}grab(){this._target.addEventListener("keydown",this._eventHandlers.keydown),this._target.addEventListener("keyup",this._eventHandlers.keyup),window.addEventListener("blur",this._eventHandlers.blur)}ungrab(){this._target.removeEventListener("keydown",this._eventHandlers.keydown),this._target.removeEventListener("keyup",this._eventHandlers.keyup),window.removeEventListener("blur",this._eventHandlers.blur),this._allKeysUp()}};var mt=class{constructor(){this._target=null,this._state=127,this._tracked=[],this._ignored=[],this._waitingRelease=!1,this._releaseStart=0,this._longpressTimeoutId=null,this._twoTouchTimeoutId=null,this._boundEventHandler=this._eventHandler.bind(this)}attach(t){this.detach(),this._target=t,this._target.addEventListener("touchstart",this._boundEventHandler),this._target.addEventListener("touchmove",this._boundEventHandler),this._target.addEventListener("touchend",this._boundEventHandler),this._target.addEventListener("touchcancel",this._boundEventHandler)}detach(){this._target&&(this._stopLongpressTimeout(),this._stopTwoTouchTimeout(),this._target.removeEventListener("touchstart",this._boundEventHandler),this._target.removeEventListener("touchmove",this._boundEventHandler),this._target.removeEventListener("touchend",this._boundEventHandler),this._target.removeEventListener("touchcancel",this._boundEventHandler),this._target=null)}_eventHandler(t){let e;switch(t.stopPropagation(),t.preventDefault(),t.type){case"touchstart":e=this._touchStart;break;case"touchmove":e=this._touchMove;break;case"touchend":case"touchcancel":e=this._touchEnd;break}for(let s=0;s0&&Date.now()-this._tracked[0].started>250){this._state=0,this._ignored.push(t);return}if(this._waitingRelease){this._state=0,this._ignored.push(t);return}switch(this._tracked.push({id:t,started:Date.now(),active:!0,firstX:e,firstY:s,lastX:e,lastY:s,angle:0}),this._tracked.length){case 1:this._startLongpressTimeout();break;case 2:this._state&=-26,this._stopLongpressTimeout();break;case 3:this._state&=-99;break;default:this._state=0}}_touchMove(t,e,s){let r=this._tracked.find(h=>h.id===t);if(r===void 0)return;r.lastX=e,r.lastY=s;let n=e-r.firstX,a=s-r.firstY;if((r.firstX!==r.lastX||r.firstY!==r.lastY)&&(r.angle=Math.atan2(a,n)*180/Math.PI),!this._hasDetectedGesture()){if(Math.hypot(n,a)<50)return;if(this._state&=-24,this._stopLongpressTimeout(),this._tracked.length!==1&&(this._state&=-9),this._tracked.length!==2&&(this._state&=-97),this._tracked.length===2){let h=this._tracked.find(o=>o.id!==t);if(Math.hypot(h.firstX-h.lastX,h.firstY-h.lastY)>50){let o=Math.abs(r.angle-h.angle);o=Math.abs((o+180)%360-180),o>90?this._state&=-33:this._state&=-65,this._isTwoTouchTimeoutRunning()&&this._stopTwoTouchTimeout()}else this._isTwoTouchTimeoutRunning()||this._startTwoTouchTimeout()}if(!this._hasDetectedGesture())return;this._pushEvent("gesturestart")}this._pushEvent("gesturemove")}_touchEnd(t,e,s){if(this._ignored.indexOf(t)!==-1){this._ignored.splice(this._ignored.indexOf(t),1),this._ignored.length===0&&this._tracked.length===0&&(this._state=127,this._waitingRelease=!1);return}if(!this._hasDetectedGesture()&&this._isTwoTouchTimeoutRunning()&&(this._stopTwoTouchTimeout(),this._state=0),!this._hasDetectedGesture()&&(this._state&=-105,this._state&=-17,this._stopLongpressTimeout(),!this._waitingRelease))switch(this._releaseStart=Date.now(),this._waitingRelease=!0,this._tracked.length){case 1:this._state&=-7;break;case 2:this._state&=-6;break}if(this._waitingRelease){Date.now()-this._releaseStart>250&&(this._state=0),this._tracked.some(n=>Date.now()-n.started>1e3)&&(this._state=0);let r=this._tracked.find(n=>n.id===t);if(r.active=!1,this._hasDetectedGesture())this._pushEvent("gesturestart");else if(this._state!==0)return}this._hasDetectedGesture()&&this._pushEvent("gestureend");for(let r=0;rt.active))}_startLongpressTimeout(){this._stopLongpressTimeout(),this._longpressTimeoutId=setTimeout(()=>this._longpressTimeout(),1e3)}_stopLongpressTimeout(){clearTimeout(this._longpressTimeoutId),this._longpressTimeoutId=null}_longpressTimeout(){if(this._hasDetectedGesture())throw new Error("A longpress gesture failed, conflict with a different gesture");this._state=16,this._pushEvent("gesturestart")}_startTwoTouchTimeout(){this._stopTwoTouchTimeout(),this._twoTouchTimeoutId=setTimeout(()=>this._twoTouchTimeout(),50)}_stopTwoTouchTimeout(){clearTimeout(this._twoTouchTimeoutId),this._twoTouchTimeoutId=null}_isTwoTouchTimeoutRunning(){return this._twoTouchTimeoutId!==null}_twoTouchTimeout(){if(this._tracked.length===0)throw new Error("A pinch or two drag gesture failed, no tracked touches");let t=this._getAverageMovement(),e=Math.abs(t.x),s=Math.abs(t.y),r=this._getAverageDistance(),n=Math.abs(Math.hypot(r.first.x,r.first.y)-Math.hypot(r.last.x,r.last.y));s{this._target&&(e=document.elementFromPoint(t.clientX,t.clientY),this._updateVisibility(e))},0)}_showCursor(){this._canvas.style.visibility==="hidden"&&(this._canvas.style.visibility="")}_hideCursor(){this._canvas.style.visibility!=="hidden"&&(this._canvas.style.visibility="hidden")}_shouldShowCursor(t){return t?t===this._target?!0:!(!this._target.contains(t)||window.getComputedStyle(t).cursor!=="none"):!1}_updateVisibility(t){this._captureIsActive()&&(t=document.captureElement),this._shouldShowCursor(t)?this._showCursor():this._hideCursor()}_updatePosition(){this._canvas.style.left=this._position.x+"px",this._canvas.style.top=this._position.y+"px"}_captureIsActive(){return document.captureElement&&document.documentElement.contains(document.captureElement)}};var zi=40*1024*1024,Yt={CONNECTING:"connecting",OPEN:"open",CLOSING:"closing",CLOSED:"closed"},jt={CONNECTING:[WebSocket.CONNECTING,Yt.CONNECTING],OPEN:[WebSocket.OPEN,Yt.OPEN],CLOSING:[WebSocket.CLOSING,Yt.CLOSING],CLOSED:[WebSocket.CLOSED,Yt.CLOSED]},Xr=["send","close","binaryType","onerror","onmessage","onopen","protocol","readyState"],yt=class{constructor(){this._websocket=null,this._rQi=0,this._rQlen=0,this._rQbufferSize=1024*1024*4,this._rQ=null,this._sQbufferSize=1024*10,this._sQlen=0,this._sQ=null,this._eventHandlers={message:()=>{},open:()=>{},close:()=>{},error:()=>{}}}get readyState(){let t;return this._websocket===null?"unused":(t=this._websocket.readyState,jt.CONNECTING.includes(t)?"connecting":jt.OPEN.includes(t)?"open":jt.CLOSING.includes(t)?"closing":jt.CLOSED.includes(t)?"closed":"unknown")}rQpeek8(){return this._rQ[this._rQi]}rQskipBytes(t){this._rQi+=t}rQshift8(){return this._rQshift(1)}rQshift16(){return this._rQshift(2)}rQshift32(){return this._rQshift(4)}_rQshift(t){let e=0;for(let s=t-1;s>=0;s--)e+=this._rQ[this._rQi++]<>>0}rQshiftStr(t){let e="";for(let s=0;s>8&255,this._sQ[this._sQlen++]=t>>0&255}sQpush32(t){this._sQensureSpace(4),this._sQ[this._sQlen++]=t>>24&255,this._sQ[this._sQlen++]=t>>16&255,this._sQ[this._sQlen++]=t>>8&255,this._sQ[this._sQlen++]=t>>0&255}sQpushString(t){let e=t.split("").map(s=>s.charCodeAt(0));this.sQpushBytes(new Uint8Array(e))}sQpushBytes(t){for(let e=0;et.length-e&&(s=t.length-e),this._sQ.set(t.subarray(e,e+s),this._sQlen),this._sQlen+=s,e+=s}}flush(){this._sQlen>0&&this.readyState==="open"&&(this._websocket.send(new Uint8Array(this._sQ.buffer,0,this._sQlen)),this._sQlen=0)}_sQensureSpace(t){this._sQbufferSize-this._sQlen{}}on(t,e){this._eventHandlers[t]=e}_allocateBuffers(){this._rQ=new Uint8Array(this._rQbufferSize),this._sQ=new Uint8Array(this._sQbufferSize)}init(){this._allocateBuffers(),this._rQi=0,this._websocket=null}open(t,e){this.attach(new WebSocket(t,e))}attach(t){this.init();let e=[...Object.keys(t),...Object.getOwnPropertyNames(Object.getPrototypeOf(t))];for(let s=0;s{L(">> WebSock.onopen"),this._websocket.protocol&&V("Server choose sub-protocol: "+this._websocket.protocol),this._eventHandlers.open(),L("<< WebSock.onopen")},this._websocket.onclose=s=>{L(">> WebSock.onclose"),this._eventHandlers.close(s),L("<< WebSock.onclose")},this._websocket.onerror=s=>{L(">> WebSock.onerror: "+s),this._eventHandlers.error(s),L("<< WebSock.onerror: "+s)}}close(){this._websocket&&((this.readyState==="connecting"||this.readyState==="open")&&(V("Closing WebSocket connection"),this._websocket.close()),this._websocket.onmessage=()=>{})}_expandCompactRQ(t){let e=(this._rQlen-this._rQi+t)*8,s=this._rQbufferSizezi&&(this._rQbufferSize=zi,this._rQbufferSize-(this._rQlen-this._rQi)this._rQbufferSize-this._rQlen&&this._expandCompactRQ(e.length),this._rQ.set(e,this._rQlen),this._rQlen+=e.length,this._rQlen-this._rQi>0?this._eventHandlers.message():L("Ignoring empty message")}};var Kr={Again:57349,AltLeft:56,AltRight:57400,ArrowDown:57424,ArrowLeft:57419,ArrowRight:57421,ArrowUp:57416,AudioVolumeDown:57390,AudioVolumeMute:57376,AudioVolumeUp:57392,Backquote:41,Backslash:43,Backspace:14,BracketLeft:26,BracketRight:27,BrowserBack:57450,BrowserFavorites:57446,BrowserForward:57449,BrowserHome:57394,BrowserRefresh:57447,BrowserSearch:57445,BrowserStop:57448,CapsLock:58,Comma:51,ContextMenu:57437,ControlLeft:29,ControlRight:57373,Convert:121,Copy:57464,Cut:57404,Delete:57427,Digit0:11,Digit1:2,Digit2:3,Digit3:4,Digit4:5,Digit5:6,Digit6:7,Digit7:8,Digit8:9,Digit9:10,Eject:57469,End:57423,Enter:28,Equal:13,Escape:1,F1:59,F10:68,F11:87,F12:88,F13:93,F14:94,F15:95,F16:85,F17:57347,F18:57463,F19:57348,F2:60,F20:90,F21:116,F22:57465,F23:109,F24:111,F3:61,F4:62,F5:63,F6:64,F7:65,F8:66,F9:67,Find:57409,Help:57461,Hiragana:119,Home:57415,Insert:57426,IntlBackslash:86,IntlRo:115,IntlYen:125,KanaMode:112,Katakana:120,KeyA:30,KeyB:48,KeyC:46,KeyD:32,KeyE:18,KeyF:33,KeyG:34,KeyH:35,KeyI:23,KeyJ:36,KeyK:37,KeyL:38,KeyM:50,KeyN:49,KeyO:24,KeyP:25,KeyQ:16,KeyR:19,KeyS:31,KeyT:20,KeyU:22,KeyV:47,KeyW:17,KeyX:45,KeyY:21,KeyZ:44,Lang1:114,Lang2:113,Lang3:120,Lang4:119,Lang5:118,LaunchApp1:57451,LaunchApp2:57377,LaunchMail:57452,MediaPlayPause:57378,MediaSelect:57453,MediaStop:57380,MediaTrackNext:57369,MediaTrackPrevious:57360,MetaLeft:57435,MetaRight:57436,Minus:12,NonConvert:123,NumLock:69,Numpad0:82,Numpad1:79,Numpad2:80,Numpad3:81,Numpad4:75,Numpad5:76,Numpad6:77,Numpad7:71,Numpad8:72,Numpad9:73,NumpadAdd:78,NumpadComma:126,NumpadDecimal:83,NumpadDivide:57397,NumpadEnter:57372,NumpadEqual:89,NumpadMultiply:55,NumpadParenLeft:57462,NumpadParenRight:57467,NumpadSubtract:74,Open:100,PageDown:57425,PageUp:57417,Paste:101,Pause:57414,Period:52,Power:57438,PrintScreen:84,Props:57350,Quote:40,ScrollLock:70,Semicolon:39,ShiftLeft:42,ShiftRight:54,Slash:53,Sleep:57439,Space:57,Suspend:57381,Tab:15,Undo:57351,WakeUp:57443};var R={encodingRaw:0,encodingCopyRect:1,encodingRRE:2,encodingHextile:5,encodingZlib:6,encodingTight:7,encodingZRLE:16,encodingTightPNG:-260,encodingJPEG:21,encodingH264:50,pseudoEncodingQualityLevel9:-23,pseudoEncodingQualityLevel0:-32,pseudoEncodingDesktopSize:-223,pseudoEncodingLastRect:-224,pseudoEncodingCursor:-239,pseudoEncodingQEMUExtendedKeyEvent:-258,pseudoEncodingQEMULedEvent:-261,pseudoEncodingDesktopName:-307,pseudoEncodingExtendedDesktopSize:-308,pseudoEncodingXvp:-309,pseudoEncodingFence:-312,pseudoEncodingContinuousUpdates:-313,pseudoEncodingExtendedMouseButtons:-316,pseudoEncodingCompressLevel9:-247,pseudoEncodingCompressLevel0:-256,pseudoEncodingVMwareCursor:1464686180,pseudoEncodingExtendedClipboard:3231835598};var qt=class i{constructor(){this._key=null}get algorithm(){return{name:"AES-ECB"}}static async importKey(t,e,s,r){let n=new i;return await n._importKey(t,s,r),n}async _importKey(t,e,s){this._key=await window.crypto.subtle.importKey("raw",t,{name:"AES-CBC"},e,s)}async encrypt(t,e){let s=new Uint8Array(e);if(s.length%16!==0||this._key===null)return null;let r=s.length/16;for(let n=0;n>>6;for(let n=0;n<15;n++)e[n]=t[n+1]>>6|t[n]<<2,t[n]=t[n+1]>>7|t[n]<<1;let r=[0,135,14,137];e[14]^=s>>>1,e[15]=t[15]<<2^r[s],t[15]=t[15]<<1^r[s>>1],this._k1=t,this._k2=e}async _encryptCTR(t,e){let s=await window.crypto.subtle.encrypt({name:"AES-CTR",counter:e,length:128},this._ctrKey,t);return new Uint8Array(s)}async _decryptCTR(t,e){let s=await window.crypto.subtle.decrypt({name:"AES-CTR",counter:e,length:128},this._ctrKey,t);return new Uint8Array(s)}async _computeCMAC(t,e){if(e.length!==16)return null;let s=Math.floor(t.length/16),r=Math.ceil(t.length/16),n=t.length-s*16,a=new Uint8Array((r+1)*16);if(a.set(e),a.set(t,16),n===0)for(let o=0;o<16;o++)a[s*16+o]^=this._k1[o];else{a[(s+1)*16+n]=128;for(let o=0;o<16;o++)a[(s+1)*16+o]^=this._k2[o]}let h=await window.crypto.subtle.encrypt({name:"AES-CBC",iv:this._zeroBlock},this._cbcKey,a);return h=new Uint8Array(h),h.slice(h.length-32,h.length-16)}static async importKey(t,e,s,r){let n=new i;return await n._importKey(t),n}async _importKey(t){this._rawKey=t,this._ctrKey=await window.crypto.subtle.importKey("raw",t,{name:"AES-CTR"},!1,["encrypt","decrypt"]),this._cbcKey=await window.crypto.subtle.importKey("raw",t,{name:"AES-CBC"},!1,["encrypt"]),await this._initCMAC()}async encrypt(t,e){let s=t.additionalData,r=t.iv,n=await this._computeCMAC(r,this._prefixBlock0),a=await this._encryptCTR(e,n),h=await this._computeCMAC(s,this._prefixBlock1),_=await this._computeCMAC(a,this._prefixBlock2);for(let l=0;l<16;l++)_[l]^=n[l]^h[l];let o=new Uint8Array(16+a.length);return o.set(a),o.set(_,a.length),o}async decrypt(t,e){let s=e.slice(0,e.length-16),r=t.additionalData,n=t.iv,a=e.slice(e.length-16),h=await this._computeCMAC(n,this._prefixBlock0),_=await this._computeCMAC(r,this._prefixBlock1),o=await this._computeCMAC(s,this._prefixBlock2);for(let x=0;x<16;x++)o[x]^=h[x]^_[x];if(o.length!==a.length)return null;for(let x=0;x>>3]&1<>>10,this.keys[h]|=(o&4032)>>>6,++h,this.keys[h]=(_&258048)<<12,this.keys[h]|=(_&63)<<16,this.keys[h]|=(o&258048)>>>4,this.keys[h]|=o&63,++h}}enc8(t){let e=t.slice(),s=0,r,n,a;r=e[s++]<<24|e[s++]<<16|e[s++]<<8|e[s++],n=e[s++]<<24|e[s++]<<16|e[s++]<<8|e[s++],a=(r>>>4^n)&252645135,n^=a,r^=a<<4,a=(r>>>16^n)&65535,n^=a,r^=a<<16,a=(n>>>2^r)&858993459,r^=a,n^=a<<2,a=(n>>>8^r)&16711935,r^=a,n^=a<<8,n=n<<1|n>>>31&1,a=(r^n)&2863311530,r^=a,n^=a,r=r<<1|r>>>31&1;for(let h=0,_=0;h<8;++h){a=n<<28|n>>>4,a^=this.keys[_++];let o=Tr[a&63];o|=Cr[a>>>8&63],o|=Er[a>>>16&63],o|=kr[a>>>24&63],a=n^this.keys[_++],o|=Mr[a&63],o|=Lr[a>>>8&63],o|=Ar[a>>>16&63],o|=Sr[a>>>24&63],r^=o,a=r<<28|r>>>4,a^=this.keys[_++],o=Tr[a&63],o|=Cr[a>>>8&63],o|=Er[a>>>16&63],o|=kr[a>>>24&63],a=r^this.keys[_++],o|=Mr[a&63],o|=Lr[a>>>8&63],o|=Ar[a>>>16&63],o|=Sr[a>>>24&63],n^=o}for(n=n<<31|n>>>1,a=(r^n)&2863311530,r^=a,n^=a,r=r<<31|r>>>1,a=(r>>>8^n)&16711935,n^=a,r^=a<<8,a=(r>>>2^n)&858993459,n^=a,r^=a<<2,a=(n>>>16^r)&65535,r^=a,n^=a<<16,a=(n>>>4^r)&252645135,r^=a,n^=a<<4,a=[n,r],s=0;s<8;s++)e[s]=(a[s>>>2]>>>8*(3-s%4))%256,e[s]<0&&(e[s]+=256);return e}},ei=class i{constructor(){this._cipher=null}get algorithm(){return{name:"DES-ECB"}}static importKey(t,e,s,r){let n=new i;return n._importKey(t),n}_importKey(t,e,s){this._cipher=new $t(t)}encrypt(t,e){let s=new Uint8Array(e);if(s.length%8!==0||this._cipher===null)return null;let r=s.length/8;for(let n=0;n0n;)(t&1n)===1n&&(s=s*i%e),t=t>>1n,i=i*i%e;return s}function tt(i,t=0){let e=i.toString(16);t===0&&(t=Math.ceil(e.length/2)),e=e.padStart(t*2,"0");let s=e.length/2,r=new Uint8Array(s);for(let n=0;nthis._keyBytes-11)return null;let s=new Uint8Array(this._keyBytes-e.length-3);window.crypto.getRandomValues(s);for(let h=0;hthis._keyBytes?s:this._keyBytes,a=et(ne(r),this._privateKeyBigInt,this._pBigInt);return tt(a,n).slice(0,n)}};async function Rr(i){let t="";for(let e=0;e>2);for(let e=0;e>5]|=(255&i.charCodeAt(e/8))<>5]>>>e%32&255);return t}function pa(i,t){i[t>>5]|=128<>>9<<4)]=t;let e=1732584193,s=-271733879,r=-1732584194,n=271733878;for(let a=0;a>16)+(t>>16)+(e>>16)<<16|65535&e}function ga(i,t){return i<>>32-t}var Vi=class{constructor(){this._algorithms={"AES-ECB":qt,"AES-EAX":Jt,"DES-ECB":ei,"DES-CBC":ti,"RSA-PKCS1-v1_5":ii,DH:si,MD5:Rr}}encrypt(t,e,s){if(e.algorithm.name!==t.name)throw new Error("algorithm does not match");if(typeof e.encrypt!="function")throw new Error("key does not support encryption");return e.encrypt(t,s)}decrypt(t,e,s){if(e.algorithm.name!==t.name)throw new Error("algorithm does not match");if(typeof e.decrypt!="function")throw new Error("key does not support encryption");return e.decrypt(t,s)}importKey(t,e,s,r,n){if(t!=="raw")throw new Error("key format is not supported");let a=this._algorithms[s.name];if(typeof a>"u"||typeof a.importKey!="function")throw new Error("algorithm is not supported");return a.importKey(e,s,r,n)}generateKey(t,e,s){let r=this._algorithms[t.name];if(typeof r>"u"||typeof r.generateKey!="function")throw new Error("algorithm is not supported");return r.generateKey(t,e,s)}exportKey(t,e){if(t!=="raw")throw new Error("key format is not supported");if(typeof e.exportKey!="function")throw new Error("key does not support exportKey");return e.exportKey()}digest(t,e){let s=this._algorithms[t];if(typeof s!="function")throw new Error("algorithm is not supported");return s(e)}deriveBits(t,e,s){if(e.algorithm.name!==t.name)throw new Error("algorithm does not match");if(typeof e.deriveBits!="function")throw new Error("key does not support deriveBits");return e.deriveBits(t,s)}},O=new Vi;var ni=class{constructor(){this._cipher=null,this._counter=new Uint8Array(16)}async setKey(t){this._cipher=await O.importKey("raw",t,{name:"AES-EAX"},!1,["encrypt, decrypt"])}async makeMessage(t){let e=new Uint8Array([(t.length&65280)>>>8,t.length&255]),s=await O.encrypt({name:"AES-EAX",iv:this._counter,additionalData:e},this._cipher,t);for(let n=0;n<16&&this._counter[n]++===255;n++);let r=new Uint8Array(t.length+2+16);return r.set(e),r.set(s,2),r}async receiveMessage(t,e){let s=new Uint8Array([(t&65280)>>>8,t&255]),r=await O.decrypt({name:"AES-EAX",iv:this._counter,additionalData:s},this._cipher,e);for(let n=0;n<16&&this._counter[n]++===255;n++);return r}},Xt=class extends Ee{constructor(t,e){super(),this._hasStarted=!1,this._checkSock=null,this._checkCredentials=null,this._approveServerResolve=null,this._sockReject=null,this._credentialsReject=null,this._approveServerReject=null,this._sock=t,this._getCredentials=e}_waitSockAsync(t){return new Promise((e,s)=>{let r=()=>!this._sock.rQwait("RA2",t);r()?e():(this._checkSock=()=>{r()&&(e(),this._checkSock=null,this._sockReject=null)},this._sockReject=s)})}_waitApproveKeyAsync(){return new Promise((t,e)=>{this._approveServerResolve=t,this._approveServerReject=e})}_waitCredentialsAsync(t){let e=()=>t===1&&this._getCredentials().username!==void 0&&this._getCredentials().password!==void 0?!0:t===2&&this._getCredentials().password!==void 0;return new Promise((s,r)=>{e()?s():(this._checkCredentials=()=>{e()&&(s(),this._checkCredentials=null,this._credentialsReject=null)},this._credentialsReject=r)})}checkInternalEvents(){this._checkSock!==null&&this._checkSock(),this._checkCredentials!==null&&this._checkCredentials()}approveServer(){this._approveServerResolve!==null&&(this._approveServerResolve(),this._approveServerResolve=null)}disconnect(){this._sockReject!==null&&(this._sockReject(new Error("disconnect normally")),this._sockReject=null),this._credentialsReject!==null&&(this._credentialsReject(new Error("disconnect normally")),this._credentialsReject=null),this._approveServerReject!==null&&(this._approveServerReject(new Error("disconnect normally")),this._approveServerReject=null)}async negotiateRA2neAuthAsync(){this._hasStarted=!0,await this._waitSockAsync(4);let t=this._sock.rQpeekBytes(4),e=this._sock.rQshift32();if(e<1024)throw new Error("RA2: server public key is too short: "+e);if(e>8192)throw new Error("RA2: server public key is too long: "+e);let s=Math.ceil(e/8);await this._waitSockAsync(s*2);let r=this._sock.rQshiftBytes(s),n=this._sock.rQshiftBytes(s),a=await O.importKey("raw",{n:r,e:n},{name:"RSA-PKCS1-v1_5"},!1,["encrypt"]),h=new Uint8Array(4+s*2);h.set(t),h.set(r,4),h.set(n,4+s);let _=this._waitApproveKeyAsync();this.dispatchEvent(new CustomEvent("serververification",{detail:{type:"RSA",publickey:h}})),await _;let o=2048,l=Math.ceil(o/8),x=(await O.generateKey({name:"RSA-PKCS1-v1_5",modulusLength:o,publicExponent:new Uint8Array([1,0,1])},!0,["encrypt"])).privateKey,u=await O.exportKey("raw",x),d=u.n,X=u.e,F=new Uint8Array(4+l*2);F[0]=(o&4278190080)>>>24,F[1]=(o&16711680)>>>16,F[2]=(o&65280)>>>8,F[3]=o&255,F.set(d,4),F.set(X,4+l),this._sock.sQpushBytes(F),this._sock.flush();let A=new Uint8Array(16);window.crypto.getRandomValues(A);let k=await O.encrypt({name:"RSA-PKCS1-v1_5"},a,A),T=new Uint8Array(2+s);if(T[0]=(s&65280)>>>8,T[1]=s&255,T.set(k,2),this._sock.sQpushBytes(T),this._sock.flush(),await this._waitSockAsync(2),this._sock.rQshift16()!==l)throw new Error("RA2: wrong encrypted message length");let E=this._sock.rQshiftBytes(l),K=await O.decrypt({name:"RSA-PKCS1-v1_5"},x,E);if(K===null||K.length!==16)throw new Error("RA2: corrupted server encrypted random");let C=new Uint8Array(32),I=new Uint8Array(32);C.set(K),C.set(A,16),I.set(A),I.set(K,16),C=await window.crypto.subtle.digest("SHA-1",C),C=new Uint8Array(C).slice(0,16),I=await window.crypto.subtle.digest("SHA-1",I),I=new Uint8Array(I).slice(0,16);let S=new ni;await S.setKey(C);let D=new ni;await D.setKey(I);let B=new Uint8Array(8+s*2+l*2),M=new Uint8Array(8+s*2+l*2);if(B.set(h),B.set(F,4+s*2),M.set(F),M.set(h,4+l*2),B=await window.crypto.subtle.digest("SHA-1",B),M=await window.crypto.subtle.digest("SHA-1",M),B=new Uint8Array(B),M=new Uint8Array(M),this._sock.sQpushBytes(await S.makeMessage(M)),this._sock.flush(),await this._waitSockAsync(38),this._sock.rQshift16()!==20)throw new Error("RA2: wrong server hash");let z=await D.receiveMessage(20,this._sock.rQshiftBytes(36));if(z===null)throw new Error("RA2: failed to authenticate the message");for(let Z=0;Z<20;Z++)if(z[Z]!==B[Z])throw new Error("RA2: wrong server hash");if(await this._waitSockAsync(19),this._sock.rQshift16()!==1)throw new Error("RA2: wrong subtype");let te=await D.receiveMessage(1,this._sock.rQshiftBytes(17));if(te===null)throw new Error("RA2: failed to authenticate the message");te=te[0];let Ie=this._waitCredentialsAsync(te);if(te===1)(this._getCredentials().username===void 0||this._getCredentials().password===void 0)&&this.dispatchEvent(new CustomEvent("credentialsrequired",{detail:{types:["username","password"]}}));else if(te===2)this._getCredentials().password===void 0&&this.dispatchEvent(new CustomEvent("credentialsrequired",{detail:{types:["password"]}}));else throw new Error("RA2: wrong subtype");await Ie;let oe;te===1?oe=he(this._getCredentials().username).slice(0,255):oe="";let Fe=he(this._getCredentials().password).slice(0,255),_e=new Uint8Array(oe.length+Fe.length+2);_e[0]=oe.length,_e[oe.length+1]=Fe.length;for(let Z=0;Z0;){if(n.rQwait("RAW",o))return!1;let l=e+(r-this._lines),x=n.rQshiftBytes(o,!1);if(h==8){let u=new Uint8Array(s*4);for(let d=0;d>0&3)*255/3,u[d*4+1]=(x[d]>>2&3)*255/3,u[d*4+2]=(x[d]>>4&3)*255/3,u[d*4+3]=255;x=u}for(let u=0;u0;){if(n.rQwait("RRE",12))return!1;let _=n.rQshiftBytes(4),o=n.rQshift16(),l=n.rQshift16(),x=n.rQshift16(),u=n.rQshift16();a.fillRect(t+o,e+l,x,u,_),this._subrects--}return!0}};var St=class{constructor(){this._tiles=0,this._lastsubencoding=0,this._tileBuffer=new Uint8Array(16*16*4)}decodeRect(t,e,s,r,n,a,h){for(this._tiles===0&&(this._tilesX=Math.ceil(s/16),this._tilesY=Math.ceil(r/16),this._totalTiles=this._tilesX*this._tilesY,this._tiles=this._totalTiles);this._tiles>0;){let _=1;if(n.rQwait("HEXTILE",_))return!1;let o=n.rQpeek8();if(o>30)throw new Error("Illegal hextile subencoding (subencoding: "+o+")");let l=this._totalTiles-this._tiles,x=l%this._tilesX,u=Math.floor(l/this._tilesX),d=t+x*16,X=e+u*16,F=Math.min(16,t+s-d),A=Math.min(16,e+r-X);if(o&1)_+=F*A*4;else if(o&2&&(_+=4),o&4&&(_+=4),o&8){if(_++,n.rQwait("HEXTILE",_))return!1;let k=n.rQpeekBytes(_).at(-1);o&16?_+=k*6:_+=k*2}if(n.rQwait("HEXTILE",_))return!1;if(n.rQshift8(),o===0)this._lastsubencoding&1?L(" Ignoring blank after RAW"):a.fillRect(d,X,F,A,this._background);else if(o&1){let k=F*A,T=n.rQshiftBytes(k*4,!1);for(let E=0;E>4,I=K&15,S=n.rQshift8(),D=(S>>4)+1,B=(S&15)+1;this._subTile(C,I,D,B,E)}}this._finishTile(a)}this._lastsubencoding=o,this._tiles--}return!0}_startTile(t,e,s,r,n){this._tileX=t,this._tileY=e,this._tileW=s,this._tileH=r;let a=n[0],h=n[1],_=n[2],o=this._tileBuffer;for(let l=0;l>o&1&&(this._zlibs[o].reset(),V("Reset zlib stream "+o));this._ctl=this._ctl>>4}let _;if(this._ctl===8)_=this._fillRect(t,e,s,r,n,a,h);else if(this._ctl===9)_=this._jpegRect(t,e,s,r,n,a,h);else if(this._ctl===10)_=this._pngRect(t,e,s,r,n,a,h);else if(!(this._ctl&8))_=this._basicRect(this._ctl,t,e,s,r,n,a,h);else throw new Error("Illegal tight compression received (ctl: "+this._ctl+")");return _&&(this._ctl=null),_}_fillRect(t,e,s,r,n,a,h){if(n.rQwait("TIGHT",3))return!1;let _=n.rQshiftBytes(3);return a.fillRect(t,e,s,r,_,!1),!0}_jpegRect(t,e,s,r,n,a,h){let _=this._readData(n);return _===null?!1:(a.imageRect(t,e,s,r,"image/jpeg",_),!0)}_pngRect(t,e,s,r,n,a,h){throw new Error("PNG received in standard Tight rect")}_basicRect(t,e,s,r,n,a,h,_){if(this._filter===null)if(t&4){if(a.rQwait("TIGHT",1))return!1;this._filter=a.rQshift8()}else this._filter=0;let o=t&3,l;switch(this._filter){case 0:l=this._copyFilter(o,e,s,r,n,a,h,_);break;case 1:l=this._paletteFilter(o,e,s,r,n,a,h,_);break;case 2:l=this._gradientFilter(o,e,s,r,n,a,h,_);break;default:throw new Error("Illegal tight filter received (ctl: "+this._filter+")")}return l&&(this._filter=null),l}_copyFilter(t,e,s,r,n,a,h,_){let o=r*n*3,l;if(o===0)return!0;if(o<12){if(a.rQwait("TIGHT",o))return!1;l=a.rQshiftBytes(o)}else{if(l=this._readData(a),l===null)return!1;this._zlibs[t].setInput(l),l=this._zlibs[t].inflate(o),this._zlibs[t].setInput(null)}let x=new Uint8Array(r*n*4);for(let u=0,d=0;u=0;F--)u=(x*s+X*8+7-F)*4,d=(n[x*o+X]>>F&1)*3,_[u]=a[d],_[u+1]=a[d+1],_[u+2]=a[d+2],_[u+3]=255;for(let F=7;F>=8-s%8;F--)u=(x*s+X*8+7-F)*4,d=(n[x*o+X]>>F&1)*3,_[u]=a[d],_[u+1]=a[d+1],_[u+2]=a[d+2],_[u+3]=255}h.blitImage(t,e,s,r,_,0,!1)}_paletteRect(t,e,s,r,n,a,h){let _=this._getScratchBuffer(s*r*4),o=s*r*4;for(let l=0,x=0;l255&&(C=255);let I=l[d++]+C;x[u++]=I,k[K]=A[K],X[K]=I}x[u++]=255,F++}}return h.blitImage(e,s,r,n,x,0,!1),!0}_readData(t){if(this._len===0){if(t.rQwait("TIGHT",3))return null;let s;s=t.rQshift8(),this._len=s&127,s&128&&(s=t.rQshift8(),this._len|=(s&127)<<7,s&128&&(s=t.rQshift8(),this._len|=s<<14))}if(t.rQwait("TIGHT",this._len))return null;let e=t.rQshiftBytes(this._len,!1);return this._len=0,e}_getScratchBuffer(t){return(!this._scratchBuffer||this._scratchBuffer.length=2&&X<=16){let F=this._decodePaletteTile(X,d,u,l);a.blitImage(x,o,u,l,F,0,!1)}else if(X===128){let F=this._decodeRLETile(d);a.blitImage(x,o,u,l,F,0,!1)}else if(X>=130&&X<=255){let F=this._decodeRLEPaletteTile(X-128,d);a.blitImage(x,o,u,l,F,0,!1)}else throw new Error("Unknown subencoding: "+X)}}return this._length=0,!0}_getBitsPerPixelInPalette(t){if(t<=2)return 1;if(t<=4)return 2;if(t<=16)return 4}_readPixels(t){let e=this._pixelBuffer,s=this._inflator.inflate(3*t);for(let r=0,n=0;r>u&_;n[o]=a[X*4],n[o+1]=a[X*4+1],n[o+2]=a[X*4+2],n[o+3]=a[X*4+3],o+=4,u-=h}u<8-h&&x=128&&(a-=128,h=this._readRLELength()),a>t)throw new Error("Too big index in palette: "+a+", palette size: "+t);if(n+h>e)throw new Error("Too big rle length in palette mode: "+h+", allowed length is: "+(e-n));for(let _=0;_d[1]==192||d[1]==194);if(l==-1)throw new Error("Illegal JPEG image without SOF");o.length===0&&this._segments.splice(l+1,0,...this._cachedQuantTables),_.length===0&&this._segments.splice(l+1,0,...this._cachedHuffmanTables);let x=0;for(let d of this._segments)x+=d.length;let u=new Uint8Array(x);x=0;for(let d of this._segments)u.set(d,x),x+=d.length;return a.imageRect(t,e,s,r,"image/jpeg",u),_.length!==0&&(this._cachedHuffmanTables=_),o.length!==0&&(this._cachedQuantTables=o),this._segments=[],!0}_readSegment(t){if(t.rQwait("JPEG",2))return null;let e=t.rQshift8();if(e!=255)throw new Error("Illegal JPEG marker received (byte: "+e+")");let s=t.rQshift8();if(s>=208&&s<=217||s==1)return new Uint8Array([e,s]);if(t.rQwait("JPEG",2,2))return null;let r=t.rQshift16();if(r<2)throw new Error("Illegal JPEG length received (length: "+r+")");if(t.rQwait("JPEG",r-2,4))return null;let n=0;if(s===218)for(n+=2;;){if(t.rQwait("JPEG",r-2+n,4))return null;let h=t.rQpeekBytes(r-2+n,!1);if(h.at(-2)===255&&h.at(-1)!==0&&!(h.at(-1)>=208&&h.at(-1)<=215)){n-=2;break}n++}let a=new Uint8Array(2+r+n);return a[0]=e,a[1]=s,a[2]=r>>8,a[3]=r,a.set(t.rQshiftBytes(r-2+n,!1),4),a}};var Wi=class{constructor(t){this._data=t,this._index=0,this.profileIdc=null,this.constraintSet=null,this.levelIdc=null}_getStartSequenceLen(t){let e=this._data;return e[t+0]==0&&e[t+1]==0&&e[t+2]==0&&e[t+3]==1?4:e[t+0]==0&&e[t+1]==0&&e[t+2]==1?3:0}_indexOfNextNalUnit(t){let e=this._data;for(let s=t;sthis._handleFrame(n),error:n=>this._handleError(n)}));let r="avc1."+t.toString(16).padStart(2,"0")+e.toString(16).padStart(2,"0")+s.toString(16).padStart(2,"0");this._decoder.configure({codec:r,codedWidth:this._width,codedHeight:this._height,optimizeForLatency:!0})}_preparePendingFrame(t){let e={timestamp:t,promise:null,resolve:null,frame:null,ready:!1,keep:!1};return e.promise=new Promise(s=>{e.resolve=s}),this._pendingFrames.push(e),e}decode(t){let e=new Wi(t),s=null,r=Math.round(window.performance.now()*1e3);for(;;){let n=e.parse();if(n===null)break;if(e.profileIdc!==null&&(self._profileIdc=e.profileIdc,self._constraintSet=e.constraintSet,self._levelIdc=e.levelIdc),this._decoder===null||this._decoder.state!=="configured"){if(!n.key){W("Missing key frame. Can't decode until one arrives");continue}if(self._profileIdc===null){W("Cannot config decoder. Have not received SPS and PPS yet.");continue}this._configureDecoder(self._profileIdc,self._constraintSet,self._levelIdc)}s=this._preparePendingFrame(r);let a=new EncodedVideoChunk({timestamp:r,type:n.key?"key":"delta",data:n.frame});try{this._decoder.decode(a)}catch(h){W("Failed to decode:",h)}}return s!==null&&(s.keep=!0),s}},Tt=class{constructor(){this._tick=0,this._contexts={}}_contextId(t,e,s,r){return[t,e,s,r].join(",")}_findOldestContextId(){let t=Number.MAX_VALUE,e;for(let[s,r]of Object.entries(this._contexts))r.lastUsed=64){let h=this._findOldestContextId();delete this._contexts[h]}let a=new Zi(s,r);return this._contexts[this._contextId(t,e,s,r)]=a,a}_getContext(t,e,s,r){let n=this._contexts[this._contextId(t,e,s,r)];return n!==void 0?n:this._createContext(t,e,s,r)}_resetContext(t,e,s,r){delete this._contexts[this._contextId(t,e,s,r)]}_resetAllContexts(){this._contexts={}}decodeRect(t,e,s,r,n,a,h){if(n.rQwait("h264 header",8))return!1;let l=n.rQshift32(),x=n.rQshift32();if(n.rQwait("h264 payload",l,8))return!1;x&2?this._resetAllContexts():x&1&&this._resetContext(t,e,s,r);let u=this._getContext(t,e,s,r);if(u.lastUsed=this._tick++,l!==0){let d=n.rQshiftBytes(l,!1),X=u.decode(d);X!==null&&a.videoFrame(t,e,s,r,X)}return!0}};var ba=3,ma="rgb(40, 40, 40)",Br=17,Dr=50,Qr=19,Mt=75,Ke=50,wa=1e3,va=50,Ir=1,hi=2,Pr=6,Hr=16,Yi=19,Ur=22,Nr=30,Or=113,zr=129,Gr=256,de=1;var Vr=1<<24,li=1<<25,Wr=1<<26,Rt=1<<27,ci=1<<28,se=class i extends Ee{constructor(t,e,s){if(!t)throw new Error("Must specify target");if(!e)throw new Error("Must specify URL, WebSocket or RTCDataChannel");window.isSecureContext||U("noVNC requires a secure context (TLS). Expect crashes!"),super(),this._target=t,typeof e=="string"?this._url=e:(this._url=null,this._rawChannel=e),s=s||{},this._rfbCredentials=s.credentials||{},this._shared="shared"in s?!!s.shared:!0,this._repeaterID=s.repeaterID||"",this._wsProtocols=s.wsProtocols||[],this._rfbConnectionState="",this._rfbInitState="",this._rfbAuthScheme=-1,this._rfbCleanDisconnect=!0,this._rfbRSAAESAuthenticationState=null,this._rfbVersion=0,this._rfbMaxVersion=3.8,this._rfbTightVNC=!1,this._rfbVeNCryptState=0,this._rfbXvpVer=0,this._fbWidth=0,this._fbHeight=0,this._fbName="",this._capabilities={power:!1},this._supportsFence=!1,this._supportsContinuousUpdates=!1,this._enabledContinuousUpdates=!1,this._supportsSetDesktopSize=!1,this._screenID=0,this._screenFlags=0,this._pendingRemoteResize=!1,this._lastResize=0,this._qemuExtKeyEventSupported=!1,this._extendedPointerEventSupported=!1,this._clipboardText=null,this._clipboardServerCapabilitiesActions={},this._clipboardServerCapabilitiesFormats={},this._sock=null,this._display=null,this._flushing=!1,this._keyboard=null,this._gestures=null,this._resizeObserver=null,this._disconnTimer=null,this._resizeTimeout=null,this._mouseMoveTimer=null,this._decoders={},this._FBU={rects:0,x:0,y:0,width:0,height:0,encoding:null},this._mousePos={},this._mouseButtonMask=0,this._mouseLastMoveTime=0,this._viewportDragging=!1,this._viewportDragPos={},this._viewportHasMoved=!1,this._accumulatedWheelDeltaX=0,this._accumulatedWheelDeltaY=0,this._gestureLastTapTime=null,this._gestureFirstDoubleTapEv=null,this._gestureLastMagnitudeX=0,this._gestureLastMagnitudeY=0,this._eventHandlers={focusCanvas:this._focusCanvas.bind(this),handleResize:this._handleResize.bind(this),handleMouse:this._handleMouse.bind(this),handleWheel:this._handleWheel.bind(this),handleGesture:this._handleGesture.bind(this),handleRSAAESCredentialsRequired:this._handleRSAAESCredentialsRequired.bind(this),handleRSAAESServerVerification:this._handleRSAAESServerVerification.bind(this)},L(">> RFB.constructor"),this._screen=document.createElement("div"),this._screen.style.display="flex",this._screen.style.width="100%",this._screen.style.height="100%",this._screen.style.overflow="auto",this._screen.style.background=ma,this._canvas=document.createElement("canvas"),this._canvas.style.margin="auto",this._canvas.style.outline="none",this._canvas.width=0,this._canvas.height=0,this._canvas.tabIndex=-1,this._screen.appendChild(this._canvas),this._cursor=new vt,this._cursorImage=i.cursors.none,this._decoders[R.encodingRaw]=new Kt,this._decoders[R.encodingCopyRect]=new Ft,this._decoders[R.encodingRRE]=new kt,this._decoders[R.encodingHextile]=new St,this._decoders[R.encodingZlib]=new Et,this._decoders[R.encodingTight]=new Qe,this._decoders[R.encodingTightPNG]=new At,this._decoders[R.encodingZRLE]=new Ct,this._decoders[R.encodingJPEG]=new Lt,this._decoders[R.encodingH264]=new Tt;try{this._display=new rt(this._canvas)}catch(r){throw U("Display exception: "+r),r}this._keyboard=new bt(this._canvas),this._keyboard.onkeyevent=this._handleKeyEvent.bind(this),this._remoteCapsLock=null,this._remoteNumLock=null,this._gestures=new mt,this._sock=new yt,this._sock.on("open",this._socketOpen.bind(this)),this._sock.on("close",this._socketClose.bind(this)),this._sock.on("message",this._handleMessage.bind(this)),this._sock.on("error",this._socketError.bind(this)),this._expectedClientWidth=null,this._expectedClientHeight=null,this._resizeObserver=new ResizeObserver(this._eventHandlers.handleResize),this._updateConnectionState("connecting"),L("<< RFB.constructor"),this.dragViewport=!1,this.focusOnClick=!0,this._viewOnly=!1,this._clipViewport=!1,this._clippingViewport=!1,this._scaleViewport=!1,this._resizeSession=!1,this._showDotCursor=!1,s.showDotCursor!==void 0&&(W("Specifying showDotCursor as a RFB constructor argument is deprecated"),this._showDotCursor=s.showDotCursor),this._qualityLevel=6,this._compressionLevel=2}get viewOnly(){return this._viewOnly}set viewOnly(t){this._viewOnly=t,(this._rfbConnectionState==="connecting"||this._rfbConnectionState==="connected")&&(t?this._keyboard.ungrab():this._keyboard.grab())}get capabilities(){return this._capabilities}get clippingViewport(){return this._clippingViewport}_setClippingViewport(t){t!==this._clippingViewport&&(this._clippingViewport=t,this.dispatchEvent(new CustomEvent("clippingviewport",{detail:this._clippingViewport})))}get touchButton(){return 0}set touchButton(t){W("Using old API!")}get clipViewport(){return this._clipViewport}set clipViewport(t){this._clipViewport=t,this._updateClip()}get scaleViewport(){return this._scaleViewport}set scaleViewport(t){this._scaleViewport=t,t&&this._clipViewport&&this._updateClip(),this._updateScale(),!t&&this._clipViewport&&this._updateClip()}get resizeSession(){return this._resizeSession}set resizeSession(t){this._resizeSession=t,t&&this._requestRemoteResize()}get showDotCursor(){return this._showDotCursor}set showDotCursor(t){this._showDotCursor=t,this._refreshCursor()}get background(){return this._screen.style.background}set background(t){this._screen.style.background=t}get qualityLevel(){return this._qualityLevel}set qualityLevel(t){if(!Number.isInteger(t)||t<0||t>9){U("qualityLevel must be an integer between 0 and 9");return}this._qualityLevel!==t&&(this._qualityLevel=t,this._rfbConnectionState==="connected"&&this._sendEncodings())}get compressionLevel(){return this._compressionLevel}set compressionLevel(t){if(!Number.isInteger(t)||t<0||t>9){U("compressionLevel must be an integer between 0 and 9");return}this._compressionLevel!==t&&(this._compressionLevel=t,this._rfbConnectionState==="connected"&&this._sendEncodings())}disconnect(){this._updateConnectionState("disconnecting"),this._sock.off("error"),this._sock.off("message"),this._sock.off("open"),this._rfbRSAAESAuthenticationState!==null&&this._rfbRSAAESAuthenticationState.disconnect()}approveServer(){this._rfbRSAAESAuthenticationState!==null&&this._rfbRSAAESAuthenticationState.approveServer()}sendCredentials(t){this._rfbCredentials=t,this._resumeAuthentication()}sendCtrlAltDel(){this._rfbConnectionState!=="connected"||this._viewOnly||(V("Sending Ctrl-Alt-Del"),this.sendKey(f.XK_Control_L,"ControlLeft",!0),this.sendKey(f.XK_Alt_L,"AltLeft",!0),this.sendKey(f.XK_Delete,"Delete",!0),this.sendKey(f.XK_Delete,"Delete",!1),this.sendKey(f.XK_Alt_L,"AltLeft",!1),this.sendKey(f.XK_Control_L,"ControlLeft",!1))}machineShutdown(){this._xvpOp(1,2)}machineReboot(){this._xvpOp(1,3)}machineReset(){this._xvpOp(1,4)}sendKey(t,e,s){if(this._rfbConnectionState!=="connected"||this._viewOnly)return;if(s===void 0){this.sendKey(t,e,!0),this.sendKey(t,e,!1);return}let r=Kr[e];if(this._qemuExtKeyEventSupported&&r)t=t||0,V("Sending key ("+(s?"down":"up")+"): keysym "+t+", scancode "+r),i.messages.QEMUExtendedKeyEvent(this._sock,t,s,r);else{if(!t)return;V("Sending keysym ("+(s?"down":"up")+"): "+t),i.messages.keyEvent(this._sock,t,s?1:0)}}focus(t){this._canvas.focus(t)}blur(){this._canvas.blur()}clipboardPasteFrom(t){if(!(this._rfbConnectionState!=="connected"||this._viewOnly))if(this._clipboardServerCapabilitiesFormats[de]&&this._clipboardServerCapabilitiesActions[Rt])this._clipboardText=t,i.messages.extendedClipboardNotify(this._sock,[de]);else{let e,s,r;e=0;for(let n of t)e++;r=new Uint8Array(e),s=0;for(let n of t){let a=n.codePointAt(0);a>255&&(a=63),r[s++]=a}i.messages.clientCutText(this._sock,r)}}getImageData(){return this._display.getImageData()}toDataURL(t,e){return this._display.toDataURL(t,e)}toBlob(t,e,s){return this._display.toBlob(t,e,s)}_connect(){if(L(">> RFB.connect"),this._url)V(`connecting to ${this._url}`),this._sock.open(this._url,this._wsProtocols);else{if(V(`attaching ${this._rawChannel} to Websock`),this._sock.attach(this._rawChannel),this._sock.readyState==="closed")throw Error("Cannot use already closed WebSocket/RTCDataChannel");this._sock.readyState==="open"&&this._socketOpen()}this._target.appendChild(this._screen),this._gestures.attach(this._canvas),this._cursor.attach(this._canvas),this._refreshCursor(),this._resizeObserver.observe(this._screen),this._canvas.addEventListener("mousedown",this._eventHandlers.focusCanvas),this._canvas.addEventListener("touchstart",this._eventHandlers.focusCanvas),this._canvas.addEventListener("mousedown",this._eventHandlers.handleMouse),this._canvas.addEventListener("mouseup",this._eventHandlers.handleMouse),this._canvas.addEventListener("mousemove",this._eventHandlers.handleMouse),this._canvas.addEventListener("click",this._eventHandlers.handleMouse),this._canvas.addEventListener("contextmenu",this._eventHandlers.handleMouse),this._canvas.addEventListener("wheel",this._eventHandlers.handleWheel),this._canvas.addEventListener("gesturestart",this._eventHandlers.handleGesture),this._canvas.addEventListener("gesturemove",this._eventHandlers.handleGesture),this._canvas.addEventListener("gestureend",this._eventHandlers.handleGesture),L("<< RFB.connect")}_disconnect(){L(">> RFB.disconnect"),this._cursor.detach(),this._canvas.removeEventListener("gesturestart",this._eventHandlers.handleGesture),this._canvas.removeEventListener("gesturemove",this._eventHandlers.handleGesture),this._canvas.removeEventListener("gestureend",this._eventHandlers.handleGesture),this._canvas.removeEventListener("wheel",this._eventHandlers.handleWheel),this._canvas.removeEventListener("mousedown",this._eventHandlers.handleMouse),this._canvas.removeEventListener("mouseup",this._eventHandlers.handleMouse),this._canvas.removeEventListener("mousemove",this._eventHandlers.handleMouse),this._canvas.removeEventListener("click",this._eventHandlers.handleMouse),this._canvas.removeEventListener("contextmenu",this._eventHandlers.handleMouse),this._canvas.removeEventListener("mousedown",this._eventHandlers.focusCanvas),this._canvas.removeEventListener("touchstart",this._eventHandlers.focusCanvas),this._resizeObserver.disconnect(),this._keyboard.ungrab(),this._gestures.detach(),this._sock.close();try{this._target.removeChild(this._screen)}catch(t){if(t.name!=="NotFoundError")throw t}clearTimeout(this._resizeTimeout),clearTimeout(this._mouseMoveTimer),L("<< RFB.disconnect")}_socketOpen(){this._rfbConnectionState==="connecting"&&this._rfbInitState===""?(this._rfbInitState="ProtocolVersion",L("Starting VNC handshake")):this._fail("Unexpected server connection while "+this._rfbConnectionState)}_socketClose(t){L("WebSocket on-close event");let e="";switch(t.code&&(e="(code: "+t.code,t.reason&&(e+=", reason: "+t.reason),e+=")"),this._rfbConnectionState){case"connecting":this._fail("Connection closed "+e);break;case"connected":this._updateConnectionState("disconnecting"),this._updateConnectionState("disconnected");break;case"disconnecting":this._updateConnectionState("disconnected");break;case"disconnected":this._fail("Unexpected server disconnect when already disconnected "+e);break;default:this._fail("Unexpected server disconnect before connecting "+e);break}this._sock.off("close"),this._rawChannel=null}_socketError(t){W("WebSocket on-error event")}_focusCanvas(t){this.focusOnClick&&this.focus({preventScroll:!0})}_setDesktopName(t){this._fbName=t,this.dispatchEvent(new CustomEvent("desktopname",{detail:{name:this._fbName}}))}_saveExpectedClientSize(){this._expectedClientWidth=this._screen.clientWidth,this._expectedClientHeight=this._screen.clientHeight}_currentClientSize(){return[this._screen.clientWidth,this._screen.clientHeight]}_clientHasExpectedSize(){let[t,e]=this._currentClientSize();return t==this._expectedClientWidth&&e==this._expectedClientHeight}_handleResize(){this._clientHasExpectedSize()||(window.requestAnimationFrame(()=>{this._updateClip(),this._updateScale(),this._saveExpectedClientSize()}),this._requestRemoteResize())}_updateClip(){let t=this._display.clipViewport,e=this._clipViewport;if(this._scaleViewport&&(e=!1),t!==e&&(this._display.clipViewport=e),e){let s=this._screenSize();this._display.viewportChangeSize(s.w,s.h),this._fixScrollbars(),this._setClippingViewport(s.w{U("Disconnection timed out."),this._updateConnectionState("disconnected")},ba*1e3);break;case"disconnected":this.dispatchEvent(new CustomEvent("disconnect",{detail:{clean:this._rfbCleanDisconnect}}));break}}_fail(t){switch(this._rfbConnectionState){case"disconnecting":U("Failed when disconnecting: "+t);break;case"connected":U("Failed while connected: "+t);break;case"connecting":U("Failed when connecting: "+t);break;default:U("RFB failure: "+t);break}return this._rfbCleanDisconnect=!1,this._updateConnectionState("disconnecting"),this._updateConnectionState("disconnected"),!1}_setCapability(t,e){this._capabilities[t]=e,this.dispatchEvent(new CustomEvent("capabilities",{detail:{capabilities:this._capabilities}}))}_handleMessage(){if(this._sock.rQwait("message",1)){W("handleMessage called on an empty receive queue");return}switch(this._rfbConnectionState){case"disconnected":U("Got data while disconnected");break;case"connected":for(;!(this._flushing||!this._normalMsg()||this._sock.rQwait("message",1)););break;case"connecting":for(;this._rfbConnectionState==="connecting"&&this._initMsg(););break;default:U("Got data while in an invalid state");break}}_handleKeyEvent(t,e,s,r,n){e=="CapsLock"&&s&&(this._remoteCapsLock=null),this._remoteCapsLock!==null&&n!==null&&this._remoteCapsLock!==n&&s&&(L("Fixing remote caps lock"),this.sendKey(f.XK_Caps_Lock,"CapsLock",!0),this.sendKey(f.XK_Caps_Lock,"CapsLock",!1),this._remoteCapsLock=null),e=="NumLock"&&s&&(this._remoteNumLock=null),this._remoteNumLock!==null&&r!==null&&this._remoteNumLock!==r&&s&&(L("Fixing remote num lock"),this.sendKey(f.XK_Num_Lock,"NumLock",!0),this.sendKey(f.XK_Num_Lock,"NumLock",!1),this._remoteNumLock=null),this.sendKey(t,e,s)}static _convertButtonMask(t){let e={0:1,1:4,2:2,3:128,4:256},s=0;for(let r=0;r<5;r++)t&1<st||Math.abs(a)>st)&&(this._viewportHasMoved=!0,this._viewportDragPos={x:e.x,y:e.y},this._display.viewportChangePos(n,a));break}this._handleMouseMove(e.x,e.y);break}}_handleMouseButton(t,e,s){this._flushMouseMoveTimer(t,e),this._mouseButtonMask=s,this._sendMouse(t,e,this._mouseButtonMask)}_handleMouseMove(t,e){if(this._mousePos={x:t,y:e},this._mouseMoveTimer==null){let s=Date.now()-this._mouseLastMoveTime;s>Br?(this._sendMouse(t,e,this._mouseButtonMask),this._mouseLastMoveTime=Date.now()):this._mouseMoveTimer=setTimeout(()=>{this._handleDelayedMouseMove()},Br-s)}}_handleDelayedMouseMove(){this._mouseMoveTimer=null,this._sendMouse(this._mousePos.x,this._mousePos.y,this._mouseButtonMask),this._mouseLastMoveTime=Date.now()}_sendMouse(t,e,s){if(this._rfbConnectionState!=="connected"||this._viewOnly)return;if(s&32768)throw new Error("Illegal mouse button mask (mask: "+s+")");let r=s&32640;this._extendedPointerEventSupported&&r?i.messages.extendedPointerEvent(this._sock,this._display.absX(t),this._display.absY(e),s):i.messages.pointerEvent(this._sock,this._display.absX(t),this._display.absY(e),s)}_handleWheel(t){if(this._rfbConnectionState!=="connected"||this._viewOnly)return;t.stopPropagation(),t.preventDefault();let e=Ne(t.clientX,t.clientY,this._canvas),s=i._convertButtonMask(t.buttons),r=t.deltaX,n=t.deltaY;t.deltaMode!==0&&(r*=Qr,n*=Qr),this._accumulatedWheelDeltaX+=r,this._accumulatedWheelDeltaY+=n,Math.abs(this._accumulatedWheelDeltaX)>=Dr&&(this._accumulatedWheelDeltaX<0?(this._handleMouseButton(e.x,e.y,s|32),this._handleMouseButton(e.x,e.y,s)):this._accumulatedWheelDeltaX>0&&(this._handleMouseButton(e.x,e.y,s|64),this._handleMouseButton(e.x,e.y,s)),this._accumulatedWheelDeltaX=0),Math.abs(this._accumulatedWheelDeltaY)>=Dr&&(this._accumulatedWheelDeltaY<0?(this._handleMouseButton(e.x,e.y,s|8),this._handleMouseButton(e.x,e.y,s)):this._accumulatedWheelDeltaY>0&&(this._handleMouseButton(e.x,e.y,s|16),this._handleMouseButton(e.x,e.y,s)),this._accumulatedWheelDeltaY=0)}_fakeMouseMove(t,e,s){this._handleMouseMove(e,s),this._cursor.move(t.detail.clientX,t.detail.clientY)}_handleTapEvent(t,e){let s=Ne(t.detail.clientX,t.detail.clientY,this._canvas);if(this._gestureLastTapTime!==null&&Date.now()-this._gestureLastTapTimest||Math.abs(n)>st)&&(this._viewportHasMoved=!0,this._viewportDragPos={x:s.x,y:s.y},this._display.viewportChangePos(r,n))}else this._fakeMouseMove(t,s.x,s.y);break;case"twodrag":for(this._fakeMouseMove(t,s.x,s.y);t.detail.magnitudeY-this._gestureLastMagnitudeY>Ke;)this._handleMouseButton(s.x,s.y,8),this._handleMouseButton(s.x,s.y,0),this._gestureLastMagnitudeY+=Ke;for(;t.detail.magnitudeY-this._gestureLastMagnitudeY<-Ke;)this._handleMouseButton(s.x,s.y,16),this._handleMouseButton(s.x,s.y,0),this._gestureLastMagnitudeY-=Ke;for(;t.detail.magnitudeX-this._gestureLastMagnitudeX>Ke;)this._handleMouseButton(s.x,s.y,32),this._handleMouseButton(s.x,s.y,0),this._gestureLastMagnitudeX+=Ke;for(;t.detail.magnitudeX-this._gestureLastMagnitudeX<-Ke;)this._handleMouseButton(s.x,s.y,64),this._handleMouseButton(s.x,s.y,0),this._gestureLastMagnitudeX-=Ke;break;case"pinch":if(this._fakeMouseMove(t,s.x,s.y),e=Math.hypot(t.detail.magnitudeX,t.detail.magnitudeY),Math.abs(e-this._gestureLastMagnitudeX)>Mt){for(this._handleKeyEvent(f.XK_Control_L,"ControlLeft",!0);e-this._gestureLastMagnitudeX>Mt;)this._handleMouseButton(s.x,s.y,8),this._handleMouseButton(s.x,s.y,0),this._gestureLastMagnitudeX+=Mt;for(;e-this._gestureLastMagnitudeX<-Mt;)this._handleMouseButton(s.x,s.y,16),this._handleMouseButton(s.x,s.y,0),this._gestureLastMagnitudeX-=Mt}this._handleKeyEvent(f.XK_Control_L,"ControlLeft",!1);break}break;case"gestureend":switch(t.detail.type){case"onetap":case"twotap":case"threetap":case"pinch":case"twodrag":break;case"drag":this.dragViewport?this._viewportDragging=!1:(this._fakeMouseMove(t,s.x,s.y),this._handleMouseButton(s.x,s.y,0));break;case"longpress":if(this._viewportHasMoved)break;this.dragViewport&&!this._viewportHasMoved?(this._fakeMouseMove(t,s.x,s.y),this._handleMouseButton(s.x,s.y,4),this._handleMouseButton(s.x,s.y,0),this._viewportDragging=!1):(this._fakeMouseMove(t,s.x,s.y),this._handleMouseButton(s.x,s.y,0));break}break}}_flushMouseMoveTimer(t,e){this._mouseMoveTimer!==null&&(clearTimeout(this._mouseMoveTimer),this._mouseMoveTimer=null,this._sendMouse(t,e,this._mouseButtonMask))}_negotiateProtocolVersion(){if(this._sock.rQwait("version",12))return!1;let t=this._sock.rQshiftStr(12).substr(4,7);V("Server ProtocolVersion: "+t);let e=0;switch(t){case"000.000":e=1;break;case"003.003":case"003.006":this._rfbVersion=3.3;break;case"003.007":this._rfbVersion=3.7;break;case"003.008":case"003.889":case"004.000":case"004.001":case"005.000":this._rfbVersion=3.8;break;default:return this._fail("Invalid server version "+t)}if(e){let r="ID:"+this._repeaterID;for(;r.length<250;)r+="\0";return this._sock.sQpushString(r),this._sock.flush(),!0}this._rfbVersion>this._rfbMaxVersion&&(this._rfbVersion=this._rfbMaxVersion);let s="00"+parseInt(this._rfbVersion,10)+".00"+this._rfbVersion*10%10;this._sock.sQpushString("RFB "+s+` +`),this._sock.flush(),L("Sent ProtocolVersion: "+s),this._rfbInitState="Security"}_isSupportedSecurityType(t){return[Ir,hi,Pr,Hr,Yi,Ur,Nr,Or,Gr].includes(t)}_negotiateSecurity(){if(this._rfbVersion>=3.7){let t=this._sock.rQshift8();if(this._sock.rQwait("security type",t,1))return!1;if(t===0)return this._rfbInitState="SecurityReason",this._securityContext="no security types",this._securityStatus=1,!0;let e=this._sock.rQshiftBytes(t);L("Server security types: "+e),this._rfbAuthScheme=-1;for(let s of e)if(this._isSupportedSecurityType(s)){this._rfbAuthScheme=s;break}if(this._rfbAuthScheme===-1)return this._fail("Unsupported security types (types: "+e+")");this._sock.sQpush8(this._rfbAuthScheme),this._sock.flush()}else{if(this._sock.rQwait("security scheme",4))return!1;if(this._rfbAuthScheme=this._sock.rQshift32(),this._rfbAuthScheme==0)return this._rfbInitState="SecurityReason",this._securityContext="authentication scheme",this._securityStatus=1,!0}return this._rfbInitState="Authentication",L("Authenticating using scheme: "+this._rfbAuthScheme),!0}_handleSecurityReason(){if(this._sock.rQwait("reason length",4))return!1;let t=this._sock.rQshift32(),e="";if(t>0){if(this._sock.rQwait("reason",t,4))return!1;e=this._sock.rQshiftStr(t)}return e!==""?(this.dispatchEvent(new CustomEvent("securityfailure",{detail:{status:this._securityStatus,reason:e}})),this._fail("Security negotiation failed on "+this._securityContext+" (reason: "+e+")")):(this.dispatchEvent(new CustomEvent("securityfailure",{detail:{status:this._securityStatus}})),this._fail("Security negotiation failed on "+this._securityContext))}_negotiateXvpAuth(){return this._rfbCredentials.username===void 0||this._rfbCredentials.password===void 0||this._rfbCredentials.target===void 0?(this.dispatchEvent(new CustomEvent("credentialsrequired",{detail:{types:["username","password","target"]}})),!1):(this._sock.sQpush8(this._rfbCredentials.username.length),this._sock.sQpush8(this._rfbCredentials.target.length),this._sock.sQpushString(this._rfbCredentials.username),this._sock.sQpushString(this._rfbCredentials.target),this._sock.flush(),this._rfbAuthScheme=hi,this._negotiateAuthentication())}_negotiateVeNCryptAuth(){if(this._rfbVeNCryptState==0){if(this._sock.rQwait("vencrypt version",2))return!1;let t=this._sock.rQshift8(),e=this._sock.rQshift8();if(!(t==0&&e==2))return this._fail("Unsupported VeNCrypt version "+t+"."+e);this._sock.sQpush8(0),this._sock.sQpush8(2),this._sock.flush(),this._rfbVeNCryptState=1}if(this._rfbVeNCryptState==1){if(this._sock.rQwait("vencrypt ack",1))return!1;let t=this._sock.rQshift8();if(t!=0)return this._fail("VeNCrypt failure "+t);this._rfbVeNCryptState=2}if(this._rfbVeNCryptState==2){if(this._sock.rQwait("vencrypt subtypes length",1))return!1;let t=this._sock.rQshift8();if(t<1)return this._fail("VeNCrypt subtypes empty");this._rfbVeNCryptSubtypesLength=t,this._rfbVeNCryptState=3}if(this._rfbVeNCryptState==3){if(this._sock.rQwait("vencrypt subtypes",4*this._rfbVeNCryptSubtypesLength))return!1;let t=[];for(let e=0;e0&&this._sock.rQwait("tunnel capabilities",16*r,4))return!1;if(this._rfbTightVNC=!0,r>0)return this._negotiateTightTunnels(r),!1}if(this._sock.rQwait("sub auth count",4))return!1;let t=this._sock.rQshift32();if(t===0)return this._rfbInitState="SecurityResult",!0;if(this._sock.rQwait("sub auth capabilities",16*t,4))return!1;let e={STDVNOAUTH__:1,STDVVNCAUTH_:2,TGHTULGNAUTH:129},s=[];for(let r=0;rthis._rfbCredentials),this._rfbRSAAESAuthenticationState.addEventListener("serververification",this._eventHandlers.handleRSAAESServerVerification),this._rfbRSAAESAuthenticationState.addEventListener("credentialsrequired",this._eventHandlers.handleRSAAESCredentialsRequired)),this._rfbRSAAESAuthenticationState.checkInternalEvents(),this._rfbRSAAESAuthenticationState.hasStarted||this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync().catch(t=>{t.message!=="disconnect normally"&&this._fail(t.message)}).then(()=>(this._rfbInitState="SecurityResult",!0)).finally(()=>{this._rfbRSAAESAuthenticationState.removeEventListener("serververification",this._eventHandlers.handleRSAAESServerVerification),this._rfbRSAAESAuthenticationState.removeEventListener("credentialsrequired",this._eventHandlers.handleRSAAESCredentialsRequired),this._rfbRSAAESAuthenticationState=null}),!1}_negotiateMSLogonIIAuth(){if(this._sock.rQwait("mslogonii dh param",24))return!1;if(this._rfbCredentials.username===void 0||this._rfbCredentials.password===void 0)return this.dispatchEvent(new CustomEvent("credentialsrequired",{detail:{types:["username","password"]}})),!1;let t=this._sock.rQshiftBytes(8),e=this._sock.rQshiftBytes(8),s=this._sock.rQshiftBytes(8),r=O.generateKey({name:"DH",g:t,p:e},!0,["deriveBits"]),n=O.exportKey("raw",r.publicKey),a=O.deriveBits({name:"DH",public:s},r.privateKey,64),h=O.importKey("raw",a,{name:"DES-CBC"},!1,["encrypt"]),_=he(this._rfbCredentials.username).substring(0,255),o=he(this._rfbCredentials.password).substring(0,63),l=new Uint8Array(256),x=new Uint8Array(64);window.crypto.getRandomValues(l),window.crypto.getRandomValues(x);for(let u=0;u<_.length;u++)l[u]=_.charCodeAt(u);l[_.length]=0;for(let u=0;u=3.8?this._rfbInitState="SecurityResult":this._rfbInitState="ClientInitialisation",!0;case Ur:return this._negotiateXvpAuth();case Nr:return this._negotiateARDAuth();case hi:return this._negotiateStdVNCAuth();case Hr:return this._negotiateTightAuth();case Yi:return this._negotiateVeNCryptAuth();case Gr:return this._negotiatePlainAuth();case zr:return this._negotiateTightUnixAuth();case Pr:return this._negotiateRA2neAuth();case Or:return this._negotiateMSLogonIIAuth();default:return this._fail("Unsupported auth scheme (scheme: "+this._rfbAuthScheme+")")}}_handleSecurityResult(){if(this._sock.rQwait("VNC auth response ",4))return!1;let t=this._sock.rQshift32();return t===0?(this._rfbInitState="ClientInitialisation",L("Authentication OK"),!0):this._rfbVersion>=3.8?(this._rfbInitState="SecurityReason",this._securityContext="security result",this._securityStatus=t,!0):(this.dispatchEvent(new CustomEvent("securityfailure",{detail:{status:t}})),this._fail("Security handshake failed"))}_negotiateServerInit(){if(this._sock.rQwait("server initialization",24))return!1;let t=this._sock.rQshift16(),e=this._sock.rQshift16(),s=this._sock.rQshift8(),r=this._sock.rQshift8(),n=this._sock.rQshift8(),a=this._sock.rQshift8(),h=this._sock.rQshift16(),_=this._sock.rQshift16(),o=this._sock.rQshift16(),l=this._sock.rQshift8(),x=this._sock.rQshift8(),u=this._sock.rQshift8();this._sock.rQskipBytes(3);let d=this._sock.rQshift32();if(this._sock.rQwait("server init name",d,24))return!1;let X=this._sock.rQshiftStr(d);if(X=Bt(X,!0),this._rfbTightVNC){if(this._sock.rQwait("TightVNC extended server init header",8,24+d))return!1;let F=this._sock.rQshift16(),A=this._sock.rQshift16(),k=this._sock.rQshift16();this._sock.rQskipBytes(2);let T=(F+A+k)*16;if(this._sock.rQwait("TightVNC extended server init header",T,32+d))return!1;this._sock.rQskipBytes(16*F),this._sock.rQskipBytes(16*A),this._sock.rQskipBytes(16*k)}return V("Screen: "+t+"x"+e+", bpp: "+s+", depth: "+r+", bigEndian: "+n+", trueColor: "+a+", redMax: "+h+", greenMax: "+_+", blueMax: "+o+", redShift: "+l+", greenShift: "+x+", blueShift: "+u),this._setDesktopName(X),this._resize(t,e),this._viewOnly||this._keyboard.grab(),this._fbDepth=24,this._fbName==="Intel(r) AMT KVM"&&(W("Intel AMT KVM only supports 8/16 bit depths. Using low color mode."),this._fbDepth=8),i.messages.pixelFormat(this._sock,this._fbDepth,!0),this._sendEncodings(),i.messages.fbUpdateRequest(this._sock,!1,0,0,this._fbWidth,this._fbHeight),this._updateConnectionState("connected"),!0}_sendEncodings(){let t=[];t.push(R.encodingCopyRect),this._fbDepth==24&&(fi&&t.push(R.encodingH264),t.push(R.encodingTight),t.push(R.encodingTightPNG),t.push(R.encodingZRLE),t.push(R.encodingJPEG),t.push(R.encodingHextile),t.push(R.encodingRRE),t.push(R.encodingZlib)),t.push(R.encodingRaw),t.push(R.pseudoEncodingQualityLevel0+this._qualityLevel),t.push(R.pseudoEncodingCompressLevel0+this._compressionLevel),t.push(R.pseudoEncodingDesktopSize),t.push(R.pseudoEncodingLastRect),t.push(R.pseudoEncodingQEMUExtendedKeyEvent),t.push(R.pseudoEncodingQEMULedEvent),t.push(R.pseudoEncodingExtendedDesktopSize),t.push(R.pseudoEncodingXvp),t.push(R.pseudoEncodingFence),t.push(R.pseudoEncodingContinuousUpdates),t.push(R.pseudoEncodingDesktopName),t.push(R.pseudoEncodingExtendedClipboard),t.push(R.pseudoEncodingExtendedMouseButtons),this._fbDepth==24&&(t.push(R.pseudoEncodingVMwareCursor),t.push(R.pseudoEncodingCursor)),i.messages.clientEncodings(this._sock,t)}_initMsg(){switch(this._rfbInitState){case"ProtocolVersion":return this._negotiateProtocolVersion();case"Security":return this._negotiateSecurity();case"Authentication":return this._negotiateAuthentication();case"SecurityResult":return this._handleSecurityResult();case"SecurityReason":return this._handleSecurityReason();case"ClientInitialisation":return this._sock.sQpush8(this._shared?1:0),this._sock.flush(),this._rfbInitState="ServerInitialisation",!0;case"ServerInitialisation":return this._negotiateServerInit();default:return this._fail("Unknown init state (state: "+this._rfbInitState+")")}}_resumeAuthentication(){setTimeout(this._initMsg.bind(this),0)}_handleSetColourMapMsg(){return L("SetColorMapEntries"),this._fail("Unexpected SetColorMapEntries message")}_handleServerCutText(){if(L("ServerCutText"),this._sock.rQwait("ServerCutText header",7,1))return!1;this._sock.rQskipBytes(3);let t=this._sock.rQshift32();if(t=it(t),this._sock.rQwait("ServerCutText content",Math.abs(t),8))return!1;if(t>=0){let e=this._sock.rQshiftStr(t);if(this._viewOnly)return!0;this.dispatchEvent(new CustomEvent("clipboard",{detail:{text:e}}))}else{t=Math.abs(t);let e=this._sock.rQshift32(),s=e&65535,r=e&4278190080;if(!!(r&Vr)){this._clipboardServerCapabilitiesFormats={},this._clipboardServerCapabilitiesActions={};for(let h=0;h<=15;h++){let _=1<0&&_.charAt(_.length-1)==="\0"&&(_=_.slice(0,-1)),_=_.replaceAll(`\r +`,` +`),this.dispatchEvent(new CustomEvent("clipboard",{detail:{text:_}}))}}else return this._fail("Unexpected action in extended clipboard message: "+r)}return!0}_handleServerFenceMsg(){if(this._sock.rQwait("ServerFence header",8,1))return!1;this._sock.rQskipBytes(3);let t=this._sock.rQshift32(),e=this._sock.rQshift8();if(this._sock.rQwait("ServerFence payload",e,9))return!1;e>64&&(W("Bad payload length ("+e+") in fence response"),e=64);let s=this._sock.rQshiftStr(e);return this._supportsFence=!0,t&1<<31?(t&=3,i.messages.clientFence(this._sock,t,s),!0):this._fail("Unexpected fence response")}_handleXvpMsg(){if(this._sock.rQwait("XVP version and message",3,1))return!1;this._sock.rQskipBytes(1);let t=this._sock.rQshift8(),e=this._sock.rQshift8();switch(e){case 0:U("XVP operation failed");break;case 1:this._rfbXvpVer=t,V("XVP extensions enabled (version "+this._rfbXvpVer+")"),this._setCapability("power",!0);break;default:this._fail("Illegal server XVP message (msg: "+e+")");break}return!0}_normalMsg(){let t;this._FBU.rects>0?t=0:t=this._sock.rQshift8();let e,s;switch(t){case 0:return s=this._framebufferUpdate(),s&&!this._enabledContinuousUpdates&&i.messages.fbUpdateRequest(this._sock,!0,0,0,this._fbWidth,this._fbHeight),s;case 1:return this._handleSetColourMapMsg();case 2:return L("Bell"),this.dispatchEvent(new CustomEvent("bell",{detail:{}})),!0;case 3:return this._handleServerCutText();case 150:return e=!this._supportsContinuousUpdates,this._supportsContinuousUpdates=!0,this._enabledContinuousUpdates=!1,e&&(this._enabledContinuousUpdates=!0,this._updateContinuousUpdates(),V("Enabling continuous updates.")),!0;case 248:return this._handleServerFenceMsg();case 250:return this._handleXvpMsg();default:return this._fail("Unexpected server message (type "+t+")"),L("sock.rQpeekBytes(30): "+this._sock.rQpeekBytes(30)),!0}}_framebufferUpdate(){if(this._FBU.rects===0){if(this._sock.rQwait("FBU header",3,1))return!1;if(this._sock.rQskipBytes(1),this._FBU.rects=this._sock.rQshift16(),this._display.pending())return this._flushing=!0,this._display.flush().then(()=>{this._flushing=!1,this._sock.rQwait("message",1)||this._handleMessage()}),!1}for(;this._FBU.rects>0;){if(this._FBU.encoding===null){if(this._sock.rQwait("rect header",12))return!1;this._FBU.x=this._sock.rQshift16(),this._FBU.y=this._sock.rQshift16(),this._FBU.width=this._sock.rQshift16(),this._FBU.height=this._sock.rQshift16(),this._FBU.encoding=this._sock.rQshift32(),this._FBU.encoding>>=0}if(!this._handleRect())return!1;this._FBU.rects--,this._FBU.encoding=null}return this._display.flip(),!0}_handleRect(){switch(this._FBU.encoding){case R.pseudoEncodingLastRect:return this._FBU.rects=1,!0;case R.pseudoEncodingVMwareCursor:return this._handleVMwareCursor();case R.pseudoEncodingCursor:return this._handleCursor();case R.pseudoEncodingQEMUExtendedKeyEvent:return this._qemuExtKeyEventSupported=!0,!0;case R.pseudoEncodingDesktopName:return this._handleDesktopName();case R.pseudoEncodingDesktopSize:return this._resize(this._FBU.width,this._FBU.height),!0;case R.pseudoEncodingExtendedDesktopSize:return this._handleExtendedDesktopSize();case R.pseudoEncodingExtendedMouseButtons:return this._extendedPointerEventSupported=!0,!0;case R.pseudoEncodingQEMULedEvent:return this._handleLedEvent();default:return this._handleDataRect()}}_handleVMwareCursor(){let t=this._FBU.x,e=this._FBU.y,s=this._FBU.width,r=this._FBU.height;if(this._sock.rQwait("VMware cursor encoding",1))return!1;let n=this._sock.rQshift8();this._sock.rQshift8();let a,h=4;if(n==0){if(a=new Array(s*r*h),this._sock.rQwait("VMware cursor classic encoding",s*r*h*2,2))return!1;let o=new Array(s*r);for(let x=0;x>8&255,X=u>>16&255,F=u>>24&255;a[x*h]=d,a[x*h+1]=X,a[x*h+2]=F,a[x*h+3]=255}else(o[x]&-256)==-256?l[x]==0?(a[x*h]=0,a[x*h+1]=0,a[x*h+2]=0,a[x*h+3]=0):(l[x]&-256,a[x*h]=0,a[x*h+1]=0,a[x*h+2]=0,a[x*h+3]=255):(a[x*h]=0,a[x*h+1]=0,a[x*h+2]=0,a[x*h+3]=255)}else if(n==1){if(this._sock.rQwait("VMware cursor alpha encoding",s*r*4,2))return!1;a=new Array(s*r*h);for(let _=0;_>24&255,a[_*4+1]=o>>16&255,a[_*4+2]=o>>8&255,a[_*4+3]=o&255}}else return W("The given cursor type is not supported: "+n+" given."),!1;return this._updateCursor(a,t,e,s,r),!0}_handleCursor(){let t=this._FBU.x,e=this._FBU.y,s=this._FBU.width,r=this._FBU.height,n=s*r*4,a=Math.ceil(s/8)*r,h=n+a;if(this._sock.rQwait("cursor encoding",h))return!1;let _=this._sock.rQshiftBytes(n),o=this._sock.rQshiftBytes(a),l=new Uint8Array(s*r*4),x=0;for(let u=0;un.charCodeAt(0)),r=O.importKey("raw",s,{name:"DES-ECB"},!1,["encrypt"]);return O.encrypt({name:"DES-ECB"},r,e)}};se.messages={keyEvent(i,t,e){i.sQpush8(4),i.sQpush8(e),i.sQpush16(0),i.sQpush32(t),i.flush()},QEMUExtendedKeyEvent(i,t,e,s){function r(a){let h=s>>8,_=s&255;return h===224&&_<127?_|128:a}i.sQpush8(255),i.sQpush8(0),i.sQpush16(e),i.sQpush32(t);let n=r(s);i.sQpush32(n),i.flush()},pointerEvent(i,t,e,s){i.sQpush8(5),s=s&127,i.sQpush8(s),i.sQpush16(t),i.sQpush16(e),i.flush()},extendedPointerEvent(i,t,e,s){i.sQpush8(5);let r=s>>7&255;if(r&252)throw new Error("Invalid mouse button mask: "+s);let n=s&127;n|=128,i.sQpush8(n),i.sQpush16(t),i.sQpush16(e),i.sQpush8(r),i.flush()},_buildExtendedClipboardFlags(i,t){let e=new Uint8Array(4),s=0,r=0;for(let n=0;n>24,e[1]=0,e[2]=0,e[3]=s,e},extendedClipboardProvide(i,t,e){let s=new gt,r=[];for(let h=0;h>24&255,_.length>>16&255,_.length>>8&255,_.length&255);for(let o=0;o<_.length;o++)r.push(_.charCodeAt(o))}let n=s.deflate(new Uint8Array(r)),a=new Uint8Array(4+n.length);a.set(se.messages._buildExtendedClipboardFlags([ci],t)),a.set(n,4),se.messages.clientCutText(i,a,!0)},extendedClipboardNotify(i,t){let e=se.messages._buildExtendedClipboardFlags([Rt],t);se.messages.clientCutText(i,e,!0)},extendedClipboardRequest(i,t){let e=se.messages._buildExtendedClipboardFlags([li],t);se.messages.clientCutText(i,e,!0)},extendedClipboardCaps(i,t,e){let s=Object.keys(e),r=new Uint8Array(4+4*s.length);s.map(a=>parseInt(a)),s.sort((a,h)=>a-h),r.set(se.messages._buildExtendedClipboardFlags(t,[]));let n=4;for(let a=0;a>24,r[n+1]=e[s[a]]>>16,r[n+2]=e[s[a]]>>8,r[n+3]=e[s[a]]>>0,n+=4,r[3]|=1<16?s=32:t>8?s=16:s=8;let r=Math.floor(t/3);i.sQpush8(0),i.sQpush8(0),i.sQpush8(0),i.sQpush8(0),i.sQpush8(s),i.sQpush8(t),i.sQpush8(0),i.sQpush8(e?1:0),i.sQpush16((1<"u"&&(e=0),typeof s>"u"&&(s=0),i.sQpush8(3),i.sQpush8(t?1:0),i.sQpush16(e),i.sQpush16(s),i.sQpush16(r),i.sQpush16(n),i.flush()},xvpOp(i,t,e){i.sQpush8(250),i.sQpush8(0),i.sQpush8(t),i.sQpush8(e),i.flush()}};se.cursors={none:{rgbaPixels:new Uint8Array,w:0,h:0,hotx:0,hoty:0},dot:{rgbaPixels:new Uint8Array([255,255,255,255,0,0,0,255,255,255,255,255,0,0,0,255,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,255,255,255,255,255]),w:3,h:3,hotx:1,hoty:1}};var Lo=se;export{Lo as default}; diff --git a/mobile/screen.html b/mobile/screen.html new file mode 100644 index 0000000..9e482a3 --- /dev/null +++ b/mobile/screen.html @@ -0,0 +1,109 @@ + + + + + + +Bot 的電腦 + + + + +
+ +
Bot 的電腦
連線中…
+ + +
+
連線中…
+
+ + + +
+ + + diff --git a/relay/src/main.rs b/relay/src/main.rs index 5ca309e..17a07e9 100644 --- a/relay/src/main.rs +++ b/relay/src/main.rs @@ -29,6 +29,9 @@ use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; const PAGE: &str = include_str!("../../mobile/index.html"); +/// The bots' computer, watched and used in a browser (noVNC). +const SCREEN_PAGE: &str = include_str!("../../mobile/screen.html"); +const NOVNC: &str = include_str!("../../mobile/novnc.js"); const MANIFEST: &str = r##"{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"},{"src":"icon-192.png","sizes":"192x192","type":"image/png","purpose":"any maskable"},{"src":"icon-512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"}]}"##; /// AnyBot's icon (the same drawing as the app's), and PNGs for the iPhone home screen and Android. const ICON: &str = include_str!("../../mobile/icon.svg"); @@ -200,6 +203,10 @@ struct Relay { /// The brain each AnyBot shows (when it is a screen of another), and which lend their hands. uses: Mutex>, hands: Mutex>, + /// People watching a bots' computer: stream id → (where the host's side goes, that host). + streams: Mutex>, String)>>, + /// One-time links for a browser to watch a host's computer: ticket → (account, host, made). + tickets: Mutex>, /// 配對碼: code → (account, the AnyBot it is for, when made). Ten minutes, once. pairs: Mutex>, /// When each host last asked (in memory; saved now and then). @@ -210,6 +217,32 @@ struct Relay { contact: String, } +/// How long a link the app opens for a browser works (reconnects included). +const TICKET_LIFE: Duration = Duration::from_secs(600); + +/// How long a connection's loop waits to read before sending what is queued: 2 ms while things +/// move (a live screen), 40 ms when quiet. A fixed 40 ms added up to 40 ms to every message. +struct Pace { + moved: Instant, + busy: Option, +} + +impl Pace { + fn new() -> Pace { + Pace { moved: Instant::now() - Duration::from_secs(60), busy: None } + } + fn moved(&mut self) { + self.moved = Instant::now(); + } + fn tune(&mut self, c: &std::net::TcpStream) { + let busy = self.moved.elapsed() < Duration::from_secs(2); + if self.busy != Some(busy) { + self.busy = Some(busy); + let _ = c.set_read_timeout(Some(Duration::from_millis(if busy { 2 } else { 40 }))); + } + } +} + fn now() -> i64 { chrono::Utc::now().timestamp() } @@ -229,6 +262,8 @@ impl Relay { flights: Mutex::new(HashMap::new()), uses: Mutex::new(HashMap::new()), hands: Mutex::new(std::collections::HashSet::new()), + streams: Mutex::new(HashMap::new()), + tickets: Mutex::new(HashMap::new()), pairs: Mutex::new(HashMap::new()), seen: Mutex::new(HashMap::new()), fails: Mutex::new(HashMap::new()), @@ -294,6 +329,95 @@ impl Relay { e.1 = Instant::now(); } + /// A person's browser watching (and maybe using) an AnyBot's computer: its VNC stream goes + /// to that AnyBot over its lasting connection, and back. Signed in by the account session + /// (?t=, the phone page) or a ticket the account's app asked for (?ticket=); owners and + /// admins only. + fn vnc_viewer(self: &Arc, req: Req, mut c: std::net::TcpStream, host: &str) { + use std::io::Write; + use tungstenite::{protocol::Role, Message, WebSocket}; + let user = match req.query.get("ticket").filter(|t| !t.is_empty()) { + Some(t) => self.tickets.lock().unwrap().get(t).filter(|(_, h, made)| h == host && made.elapsed() < TICKET_LIFE).map(|(u, _, _)| u.clone()), + None => self.session(&req), + }; + let Some(user) = user else { return httpd::write_response(&mut c, &Resp::error(401, "請先登入")) }; + let allowed = self.data.lock().unwrap().hosts.iter().any(|x| x.id == host && matches!(x.role_of(&user).as_deref(), Some("owner") | Some("admin"))); + if !allowed { + return httpd::write_response(&mut c, &Resp::error(404, "找不到這台 AnyBot")); + } + let Some(to_host) = self.sockets.lock().unwrap().get(host).map(|s| s.tx.clone()) else { + return httpd::write_response(&mut c, &Resp::error(503, "這台 AnyBot 現在沒有連線")); + }; + let key = req.header("sec-websocket-key").to_string(); + if key.is_empty() { + return httpd::write_response(&mut c, &Resp::error(400, "bad request")); + } + let accept = tungstenite::handshake::derive_accept_key(key.as_bytes()); + if c.write_all(format!("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {accept}\r\n\r\n").as_bytes()).is_err() { + return; + } + let _ = c.set_nodelay(true); + let mut ws = WebSocket::from_raw_socket(c, Role::Server, None); + let sid = httpd::random_hex(8); + let (tx, rx) = channel::>(); + self.streams.lock().unwrap().insert(sid.clone(), (tx, host.to_string())); + if to_host.send(wire::pack(&json!({"t": "vopen", "sid": sid}), &[])).is_err() { + self.streams.lock().unwrap().remove(&sid); + return; + } + let (mut heard, mut pinged) = (Instant::now(), Instant::now()); + let mut pace = Pace::new(); + 'live: loop { + pace.tune(ws.get_ref()); + let up = match ws.read() { + Ok(Message::Binary(b)) => Some(("vdata", b.to_vec())), + Ok(Message::Text(t)) => Some(("vtext", t.as_bytes().to_vec())), + Ok(Message::Close(_)) => break, + Ok(_) => None, + Err(tungstenite::Error::Io(e)) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => None, + Err(_) => break, + }; + if let Some((t, b)) = up { + heard = Instant::now(); + pace.moved(); + if to_host.send(wire::pack(&json!({"t": t, "sid": sid}), &b)).is_err() { + break; + } + } + loop { + match rx.try_recv() { + Ok(b) => { + pace.moved(); + if ws.write(Message::Binary(b.into())).is_err() { + break 'live; + } + } + Err(std::sync::mpsc::TryRecvError::Empty) => break, + // The host closed it (or went away). + Err(std::sync::mpsc::TryRecvError::Disconnected) => break 'live, + } + } + if pinged.elapsed().as_secs() >= WS_PING_SECS { + pinged = Instant::now(); + if ws.write(Message::Ping(Vec::new().into())).is_err() { + break; + } + } + match ws.flush() { + Err(tungstenite::Error::Io(e)) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {} + Err(_) => break, + Ok(()) => {} + } + if heard.elapsed().as_secs() >= WS_DEAD_SECS { + break; + } + } + let _ = ws.close(None); + let _ = ws.flush(); + self.streams.lock().unwrap().remove(&sid); + let _ = to_host.send(wire::pack(&json!({"t": "vclose", "sid": sid}), &[])); + } + /// Hand a request to a host and wait for its answer. fn forward(&self, host_id: &str, method: &str, path: &str, query: &HashMap, body: &[u8], who: (&str, &str)) -> Resp { self.forward_raw(host_id, method, path, query, body, false, who, false, "").into_resp() @@ -358,6 +482,10 @@ impl Relay { fn ws(self: &Arc, req: Req, mut c: std::net::TcpStream) { use std::io::Write; use tungstenite::{protocol::Role, Message, WebSocket}; + if let Some(host) = req.path.strip_prefix("/relay/vnc/") { + let host = host.to_string(); + return self.vnc_viewer(req, c, &host); + } if req.path != "/relay/host/ws" { return httpd::write_response(&mut c, &Resp::error(404, "沒有這個網址")); } @@ -379,10 +507,13 @@ impl Relay { self.sockets.lock().unwrap().insert(h.id.clone(), Sock { tx: tx.clone(), serial }); self.seen.lock().unwrap().insert(h.id.clone(), Instant::now()); let (mut heard, mut pinged, mut marked) = (Instant::now(), Instant::now(), Instant::now()); + let mut pace = Pace::new(); 'live: loop { + pace.tune(ws.get_ref()); match ws.read() { Ok(Message::Binary(b)) => { heard = Instant::now(); + pace.moved(); self.on_frame(&h, &b, &tx); } Ok(Message::Close(_)) => break, @@ -391,6 +522,7 @@ impl Relay { Err(_) => break, } while let Ok(f) = rx.try_recv() { + pace.moved(); if ws.write(Message::Binary(f.into())).is_err() { break 'live; } @@ -414,7 +546,8 @@ impl Relay { self.seen.lock().unwrap().insert(h.id.clone(), Instant::now()); } } - // Gone: whoever waits on it hears so now, not after a timeout. + // Gone: its viewers' streams end, and whoever waits on it hears so now, not after a timeout. + self.streams.lock().unwrap().retain(|_, (_, host)| *host != h.id); { let mut socks = self.sockets.lock().unwrap(); if socks.get(&h.id).map(|s| s.serial) == Some(serial) { @@ -434,6 +567,20 @@ impl Relay { let Some((head, body)) = wire::unpack(b) else { return }; let s = |k: &str| head[k].as_str().unwrap_or("").to_string(); match head["t"].as_str().unwrap_or("") { + // A viewer's stream: only the host it was opened on may write to it. + "vdata" => { + if let Some((v, host)) = self.streams.lock().unwrap().get(&s("sid")) { + if *host == h.id { + let _ = v.send(body); + } + } + } + "vclose" => { + let mut all = self.streams.lock().unwrap(); + if all.get(&s("sid")).map(|(_, host)| *host == h.id).unwrap_or(false) { + all.remove(&s("sid")); + } + } "reply" => { // Only the host the job went to may answer it. let w = { @@ -499,6 +646,25 @@ impl Relay { ("GET", []) | ("GET", ["index.html"]) => Resp::bytes("text/html; charset=utf-8", PAGE.as_bytes().to_vec()) .with_header("Content-Security-Policy", "default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'") .with_header("Strict-Transport-Security", "max-age=31536000"), + ("GET", ["screen"]) => Resp::bytes("text/html; charset=utf-8", SCREEN_PAGE.as_bytes().to_vec()) + .with_header("Content-Security-Policy", "default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:") + .with_header("Strict-Transport-Security", "max-age=31536000"), + ("GET", ["novnc.js"]) => Resp::bytes("text/javascript; charset=utf-8", NOVNC.as_bytes().to_vec()).with_header("Cache-Control", "public, max-age=86400"), + // A link for this account's browser to watch one of its AnyBots' computer (the app + // opens it; the browser has no session of its own). + ("POST", ["relay", "screen-ticket"]) => { + let Some(h) = self.host(&req) else { return Resp::error(401, "這台電腦的金鑰無效") }; + let want = req.json()["host"].as_str().unwrap_or("").to_string(); + let ok = self.data.lock().unwrap().hosts.iter().any(|x| x.id == want && matches!(x.role_of(&h.user).as_deref(), Some("owner") | Some("admin"))); + if !ok { + return Resp::error(404, "找不到這台 AnyBot"); + } + let t = httpd::random_hex(24); + let mut all = self.tickets.lock().unwrap(); + all.retain(|_, (_, _, made)| made.elapsed() < TICKET_LIFE); + all.insert(t.clone(), (h.user.clone(), want, Instant::now())); + Resp::json(&json!({"ticket": t})) + } ("GET", ["manifest.webmanifest"]) => Resp::bytes("application/manifest+json", MANIFEST.as_bytes().to_vec()), ("GET", ["icon.svg"]) => Resp::bytes("image/svg+xml", ICON.as_bytes().to_vec()), ("GET", [name]) if ICON_PNGS.iter().any(|(n, _)| n == name) => Resp::bytes("image/png", ICON_PNGS.iter().find(|(n, _)| n == name).map(|(_, b)| b.to_vec()).unwrap_or_default()), diff --git a/src/app.rs b/src/app.rs index 41c3f34..8a8658e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -371,13 +371,6 @@ impl App { app.state.selected = 2; app.show_routines = Some(2); } - Ok("viewer") => { - crate::cloudbox::start_background(); - let _ = crate::cloudbox::ensure_running(); - app.cu.demo_live = true; - app.cu.viewer_open = true; - app.cu.fit = true; - } Ok("attach") => { app.state.selected = 1; let dir = std::env::temp_dir().join("anybot-shot-attach"); @@ -511,7 +504,6 @@ impl App { // The real box, so the 電腦 card shows its screen. crate::cloudbox::start_background(); let _ = crate::cloudbox::ensure_running(); - app.cu.demo_live = true; app.state.selected = 1; app.panel = Some(panel_ui::PanelTab::Activity); } @@ -3427,11 +3419,6 @@ impl App { ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(vec2(w, h))); } } - // Real data: the computer viewer opens a little after start, as a person would open it (a - // screen of a brain has said so by then, so it shows the brain's computer). - if self.frames == 10 && !self.demo && std::env::var("ANYBOT_SHOT_VIEW").as_deref() == Ok("viewer") { - self.open_viewer(); - } // ANYBOT_SHOT_PAGE=id: open that page late (a screen gets its pages from the brain first). if self.frames == need * 3 / 4 { if let Some(id) = std::env::var("ANYBOT_SHOT_PAGE").ok().and_then(|v| v.parse().ok()) { @@ -3767,7 +3754,6 @@ impl eframe::App for App { self.delete_dialog(ctx); self.routines_window(ctx); self.rebuild_dialog(ctx); - self.viewer_window(ctx); self.local_control_bar(ctx); self.teach_bar(ctx); self.approval_popup(ctx); diff --git a/src/app/computers_ui.rs b/src/app/computers_ui.rs index 794466b..9f78e72 100644 --- a/src/app/computers_ui.rs +++ b/src/app/computers_ui.rs @@ -7,9 +7,6 @@ use super::*; use crate::cloudbox::{self, Rebuild, Run}; -/// While the viewer is open it redraws at least this often (new pictures ask for a redraw -/// themselves as they arrive). -const VIEWER_FRAME_MS: u64 = 450; /// The floating bar stays this long after the bot's last mouse/keyboard action on this Mac. const LOCAL_BAR_SECS: u64 = 8; @@ -30,16 +27,8 @@ pub struct ComputerUi { pub confirm_reset: bool, /// 一律允許的應用程式 is expanded. pub apps_open: bool, - pub viewer_open: bool, - /// 取得控制權: the person's clicks and keys go to the box. + /// 接管 during a hand-off: this app told the bots' computer to let the person have it. pub control: bool, - pub fit: bool, - texture: Option, - frame: Arc>>, - shown_seq: u64, - frame_size: [usize; 2], - worker: Option>, - input_tx: Option>, pub confirm: Option, pub note: Arc>, disk: Arc>>, @@ -51,8 +40,6 @@ pub struct ComputerUi { teach_name: String, teach_for: Option, teach_error: String, - /// Screenshot mode: show the real box in the viewer. - pub demo_live: bool, schedule_edit: String, pub update_ready: bool, update_checked: Option, @@ -62,16 +49,6 @@ pub struct ComputerUi { thumb_img: Arc>>, thumb_busy: Arc, thumb_at: Option, - /// The viewer shows the brain's computer (this window is a screen of a brain): how to reach it. - remote: Option, -} - -enum Input { - Click(i64, i64, &'static str, u32), - Move(i64, i64), - Scroll(i64, i64, bool, u32), - Key(String), - Text(String), } fn status_color(r: &Run, p: Palette) -> Color32 { @@ -103,16 +80,6 @@ fn status_pill(ui: &mut egui::Ui, r: &Run, p: Palette) { } /// A little monitor drawn with lines (no emoji font needed). -fn monitor_icon(ui: &mut egui::Ui, size: f32, col: Color32) { - let (rect, _) = ui.allocate_exact_size(vec2(size, size), egui::Sense::hover()); - let s = size / 20.0; - let screen = egui::Rect::from_min_size(rect.min + vec2(2.0 * s, 3.0 * s), vec2(16.0 * s, 11.0 * s)); - ui.painter().rect_stroke(screen, 2.5 * s, Stroke::new(1.5, col), egui::StrokeKind::Inside); - ui.painter().line_segment([rect.min + vec2(10.0 * s, 14.0 * s), rect.min + vec2(10.0 * s, 17.0 * s)], Stroke::new(1.5, col)); - ui.painter().line_segment([rect.min + vec2(6.5 * s, 17.5 * s), rect.min + vec2(13.5 * s, 17.5 * s)], Stroke::new(1.5, col)); -} - - impl App { /// Called once at start. pub(super) fn computers_start(&mut self) { @@ -946,455 +913,67 @@ impl App { self.cu.thumb.as_ref().map(|t| (t.id(), t.size_vec2())) } + /// 看畫面: the bots' computer in the browser (noVNC), live, where the person can also take + /// over. On a screen of a brain it is the brain's computer, through the account server (a + /// link made for this once); here it is this computer's own box. pub(super) fn open_viewer(&mut self) { - self.cu.viewer_open = true; - self.cu.fit = true; + self.open_screen(false); + } + + fn open_screen(&mut self, control: bool) { self.show_settings = false; - // A screen of a brain watches the brain's computer (where its bots really work). + let name = self.state.bots.iter().find(|b| b.id == self.state.selected).map(|b| format!("{} 的電腦", b.name)).unwrap_or_else(|| "Bot 的電腦".into()); if self.mir.on { let s = self.state.remote.clone(); - self.cu.remote = Some(s.clone()); - if let Some(stop) = self.cu.worker.take() { - stop.store(true, Ordering::SeqCst); - } - self.cu.texture = None; - std::thread::spawn(move || { - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "viewer", "on": true})); - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "wake"})); + self.box_task(move || { + let ticket = crate::remote::screen_ticket(&s)?; + let enc = |t: &str| t.bytes().map(|b| if b.is_ascii_alphanumeric() { (b as char).to_string() } else { format!("%{b:02X}") }).collect::(); + let url = format!("{}/screen#host={}&ticket={ticket}&name={}{}", s.relay_url.trim_end_matches('/'), s.brain_id, enc(&name), if control { "&control=1" } else { "" }); + crate::preview::open_with_system(&url); + Ok(String::new()) }); return; } - if self.cu.remote.take().is_some() { - if let Some(stop) = self.cu.worker.take() { - stop.store(true, Ordering::SeqCst); - } - self.cu.texture = None; - } - cloudbox::set_viewer_open(true); - if !cloudbox::run_state().is_running() && !self.demo { - self.box_task(|| cloudbox::ensure_running().map(|_| String::new())); + if self.demo { + return; } + self.box_task(move || { + let url = cloudbox::viewer_url(control)?; + crate::preview::open_with_system(&url); + Ok(String::new()) + }); } - /// 接管: open the computer and take control (the bot waits for the person anyway). + /// 接管: open the computer with control (the bot waits for the person meanwhile). pub(super) fn take_over_computer(&mut self) { - self.open_viewer(); + self.open_screen(true); if self.cu.control { return; } self.cu.control = true; - match self.cu.remote.clone() { - Some(s) => { - std::thread::spawn(move || { - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "hold", "on": true})); - }); - } - None => cloudbox::HELD_BY_PERSON.store(true, Ordering::SeqCst), - } + self.hold_computer(true); } - /// 我完成了 / 跳過: the bot gets its computer back (its steps would otherwise wait for the - /// person to let go). + /// 我完成了 / 跳過: the bot gets its computer back. pub(super) fn give_back_computer(&mut self) { if !self.cu.control { return; } self.cu.control = false; - match self.cu.remote.clone() { - Some(s) => { - std::thread::spawn(move || { - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "hold", "on": false})); - }); - } - None => cloudbox::HELD_BY_PERSON.store(false, Ordering::SeqCst), - } + self.hold_computer(false); } - fn close_viewer(&mut self) { - self.cu.viewer_open = false; - self.cu.control = false; - if let Some(s) = self.cu.remote.clone() { + fn hold_computer(&self, on: bool) { + if self.mir.on { + let s = self.state.remote.clone(); std::thread::spawn(move || { - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "hold", "on": false})); - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "viewer", "on": false})); + let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "hold", "on": on})); }); } else { - cloudbox::HELD_BY_PERSON.store(false, Ordering::SeqCst); - cloudbox::set_viewer_open(false); - } - if let Some(stop) = self.cu.worker.take() { - stop.store(true, Ordering::SeqCst); - } - self.cu.input_tx = None; - } - - fn ensure_viewer_threads(&mut self, ctx: &egui::Context) { - if self.cu.worker.is_some() || (self.demo && !self.cu.demo_live) { - return; - } - let stop = Arc::new(AtomicBool::new(false)); - self.cu.worker = Some(stop.clone()); - let slot = self.cu.frame.clone(); - let c = ctx.clone(); - let s2 = stop.clone(); - let brain = self.cu.remote.clone(); - if let Some(s) = brain.clone() { - // The brain's computer: pictures and the person's input go through the account server. - let s_frames = s.clone(); - std::thread::spawn(move || { - let s = s_frames; - // Ask for the picture after the one shown: the brain answers when the screen - // changes (no fixed pause between pictures). - let mut seq = 0u64; - while !s2.load(Ordering::SeqCst) { - match crate::remote::sync_live(&s, seq) { - Ok((n, jpeg)) if n != seq || n == 0 => { - if let Ok(img) = image::load_from_memory(&jpeg) { - let img = img.to_rgba8(); - let size = [img.width() as usize, img.height() as usize]; - *slot.lock().unwrap() = Some((n.max(seq + 1), egui::ColorImage::from_rgba_unmultiplied(size, img.as_raw()))); - c.request_repaint(); - } - // At most about 8 a second over the network (an animated page - // sends all it can); a brain too old to number them: one a second. - std::thread::sleep(std::time::Duration::from_millis(if n == 0 { 1000 } else { 120 })); - seq = n; - } - Ok(_) => {} - Err(_) => { - std::thread::sleep(std::time::Duration::from_millis(1500)); - c.request_repaint(); - } - } - } - }); - let (tx, rx) = channel::(); - self.cu.input_tx = Some(tx); - std::thread::spawn(move || { - let mut last_move = Instant::now(); - while let Ok(i) = rx.recv() { - let v = match i { - Input::Click(x, y, b, n) => serde_json::json!({"kind": "click", "x": x, "y": y, "button": b, "count": n}), - // Hovering sends many moves; a few a second are enough over the network. - Input::Move(x, y) => { - if last_move.elapsed().as_millis() < 250 { - continue; - } - last_move = Instant::now(); - serde_json::json!({"kind": "move", "x": x, "y": y}) - } - Input::Scroll(x, y, down, n) => serde_json::json!({"kind": "scroll", "x": x, "y": y, "down": down, "steps": n}), - Input::Key(k) => serde_json::json!({"kind": "key", "key": k}), - Input::Text(t) => serde_json::json!({"kind": "text", "text": t}), - }; - let _ = crate::remote::sync_post(&s, "screen", &v); - } - }); - return; - } - std::thread::spawn(move || { - let mut seq = 0u64; - while !s2.load(Ordering::SeqCst) { - match cloudbox::live_frame(seq, std::time::Duration::from_secs(5)) { - Some((n, jpeg)) if n != seq => { - if let Ok(img) = image::load_from_memory(&jpeg) { - let img = img.to_rgba8(); - let size = [img.width() as usize, img.height() as usize]; - *slot.lock().unwrap() = Some((n, egui::ColorImage::from_rgba_unmultiplied(size, img.as_raw()))); - c.request_repaint(); - } - seq = n; - } - Some(_) => {} - None => { - std::thread::sleep(std::time::Duration::from_millis(1000)); - c.request_repaint(); - } - } - } - }); - let (tx, rx) = channel::(); - self.cu.input_tx = Some(tx); - std::thread::spawn(move || { - while let Ok(i) = rx.recv() { - match i { - Input::Click(x, y, b, n) => cloudbox::viewer_click(x, y, b, n), - Input::Move(x, y) => cloudbox::viewer_move(x, y), - Input::Scroll(x, y, down, n) => cloudbox::viewer_scroll(x, y, down, n), - Input::Key(k) => cloudbox::viewer_key(&k), - Input::Text(t) => cloudbox::viewer_text(&t), - } - } - }); - } - - pub(super) fn viewer_window(&mut self, ctx: &egui::Context) { - if !self.cu.viewer_open { - return; - } - self.ensure_viewer_threads(ctx); - let p = self.pal; - if let Some((seq, img)) = self.cu.frame.lock().unwrap().take() { - if seq != self.cu.shown_seq { - self.cu.shown_seq = seq; - self.cu.frame_size = img.size; - match &mut self.cu.texture { - Some(t) => t.set(img, egui::TextureOptions::LINEAR), - None => self.cu.texture = Some(ctx.load_texture("box-screen", img, egui::TextureOptions::LINEAR)), - } - } - } - let brain = self.cu.remote.is_some(); - // The brain's computer reports its state with the brain's answers. - let run = if brain { - match (self.mir.box_state["running"].as_bool().unwrap_or(false), self.mir.box_state["available"].as_bool().unwrap_or(true)) { - (true, _) => Run::Running, - (false, false) => Run::NoDocker("大腦那台沒有 Docker".into()), - _ => Run::Asleep, - } - } else { - cloudbox::run_state() - }; - let screen = ctx.content_rect(); - let open = true; - let default = vec2((screen.width() * 0.62).min(900.0), (screen.width() * 0.62).min(900.0) * 0.625 + 52.0); - let busy = if brain { self.mir.box_state["busy"].as_bool().unwrap_or(false) } else { cloudbox::is_busy() }; - let title = self.state.bot(self.state.selected).map(|b| crate::model::own_computer_name(&b.name)).unwrap_or_else(|| crate::model::BOT_COMPUTER_NAME.to_string()); - - let mut close_clicked = false; - let mut done_clicked = false; - let selected = self.state.selected; - let handoff = self.approvals.get(&selected).map(|a| a.kind == crate::providers::ApprovalKind::Handoff).unwrap_or(false); - egui::Window::new("box-viewer") - .id(egui::Id::unique("box-viewer")) - .title_bar(false) - .default_size(default) - .default_pos(screen.center() - default / 2.0) - .min_size(vec2(420.0, 300.0)) - .resizable(true) - .collapsible(false) - .frame( - Frame::new() - .fill(p.elevated) - .corner_radius(14) - .inner_margin(Margin::symmetric(14, 12)) - .stroke(Stroke::new(1.0, p.border)) - .shadow(egui::epaint::Shadow { offset: [0, 14], blur: 40, spread: 0, color: Color32::from_black_alpha(60) }), - ) - .show(ctx, |ui| { - // Buttons first, from the right; the title and state get what is left and end in - // … when it's short (the state used to slide under 「取得控制權」 in a narrow window). - ui.horizontal(|ui| { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - let (rect, r) = ui.allocate_exact_size(vec2(28.0, 28.0), egui::Sense::click()); - if r.hovered() { - ui.painter().rect_filled(rect, 8, p.hover); - } - let c = rect.center(); - let st = Stroke::new(1.5, if r.hovered() { p.text } else { p.text2 }); - ui.painter().line_segment([c + vec2(-5.0, -5.0), c + vec2(5.0, 5.0)], st); - ui.painter().line_segment([c + vec2(-5.0, 5.0), c + vec2(5.0, -5.0)], st); - if r.on_hover_text(t("關閉")).clicked() { - close_clicked = true; - } - ui.add_space(4.0); - // The bot is waiting for the person here: done, and it carries on. - if handoff && ui.add(egui::Button::new(RichText::new(t("我完成了")).strong().color(p.on_primary)).fill(p.primary).corner_radius(8).min_size(vec2(84.0, 30.0))).clicked() { - done_clicked = true; - } - if !brain && ui.add(secondary_button("全螢幕")).on_hover_text(t("在瀏覽器用 noVNC 全螢幕開啟")).clicked() { - let control = self.cu.control; - self.box_task(move || { - let url = cloudbox::viewer_url(control)?; - crate::preview::open_with_system(&url); - Ok(String::new()) - }); - } - let fit_label = if self.cu.fit { "實際大小" } else { "調整至視窗大小" }; - if ui.add(secondary_button(fit_label)).clicked() { - self.cu.fit = !self.cu.fit; - } - let (label, fill, col) = if self.cu.control { ("交還給 Bot", p.accent, Color32::WHITE) } else { ("取得控制權", p.primary, p.on_primary) }; - let btn = egui::Button::new(RichText::new(label).strong().color(col)).fill(fill).corner_radius(8).min_size(vec2(96.0, 30.0)); - if ui.add_enabled(run.is_running(), btn).on_hover_text(if self.cu.control { "Bot 可以繼續操作" } else { "你自己點擊和輸入;Bot 的操作會先暫停" }).clicked() { - self.cu.control = !self.cu.control; - match self.cu.remote.clone() { - Some(s) => { - let on = self.cu.control; - std::thread::spawn(move || { - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "hold", "on": on})); - }); - } - None => cloudbox::HELD_BY_PERSON.store(self.cu.control, Ordering::SeqCst), - } - } - ui.add_space(8.0); - ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - monitor_icon(ui, 20.0, p.text); - let note = if self.cu.control { Some(t("你正在操作,Bot 已暫停")) } else if busy { Some(t("Bot 正在操作")) } else { None }; - let room = (ui.available_width() - 96.0 - if note.is_some() { 120.0 } else { 0.0 }).max(60.0); - ui.label(RichText::new(super::sidebar_ui::shorten_width(ui, &title, room, FONT_LG)).strong().size(FONT_LG)); - if ui.available_width() > 80.0 { - status_pill(ui, &run, p); - } - if let Some(n) = note.filter(|_| ui.available_width() > 110.0) { - crate::icons::label(ui, if self.cu.control { crate::icons::Icon::Users } else { crate::icons::Icon::Bot }, &n, FONT_XS, p.accent); - } - }); - }); - }); - ui.add_space(6.0); - let avail = ui.available_size(); - match (self.cu.texture.as_ref().map(|t| t.id()), run.is_running()) { - (Some(tex_id), true) => { - let [fw, fh] = self.cu.frame_size; - let (fw, fh) = (fw.max(1) as f32, fh.max(1) as f32); - let size = if self.cu.fit { - let k = (avail.x / fw).min((avail.y.max(120.0)) / fh); - vec2(fw * k, fh * k) - } else { - vec2(fw, fh) - }; - egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { - let sense = if self.cu.control { egui::Sense::click_and_drag() } else { egui::Sense::hover() }; - // Centred in the window (it sat at the left with an empty band beside it). - let resp = if self.cu.fit { - let pad = ((ui.available_width() - size.x) / 2.0).max(0.0); - ui.horizontal(|ui| { - ui.add_space(pad); - ui.add(egui::Image::new((tex_id, size)).corner_radius(8).sense(sense)) - }) - .inner - } else { - ui.add(egui::Image::new((tex_id, size)).corner_radius(8).sense(sense)) - }; - if self.cu.control { - ui.painter().rect_stroke(resp.rect, 8, Stroke::new(2.0, p.accent), egui::StrokeKind::Outside); - self.forward_input(ui, &resp, (fw, fh)); - } - }); - } - _ => { - let (rect, _) = ui.allocate_exact_size(vec2(avail.x, avail.y.max(200.0)), egui::Sense::hover()); - ui.painter().rect_filled(rect, 8, Color32::from_rgb(0x2b, 0x2f, 0x36)); - let text = match &run { - Run::Running => "正在連線…".to_string(), - Run::NoDocker(why) => why.clone(), - Run::Failed(e) => format!("發生問題:{e}"), - other => format!("{}…", other.label()), - }; - ui.painter().text(rect.center(), egui::Align2::CENTER_CENTER, text, egui::FontId::proportional(FONT_BASE), Color32::from_gray(220)); - if matches!(run, Run::Asleep | Run::Absent) { - let b = egui::Rect::from_center_size(rect.center() + vec2(0.0, 34.0), vec2(90.0, 30.0)); - if ui.put(b, primary_button("喚醒", p)).clicked() { - match self.cu.remote.clone() { - Some(s) => { - std::thread::spawn(move || { - let _ = crate::remote::sync_post(&s, "screen", &serde_json::json!({"kind": "wake"})); - }); - } - None => self.box_task(|| cloudbox::ensure_running().map(|_| String::new())), - } - } - } - } - } - }); - if done_clicked { - self.give_back_computer(); - self.answer(ctx, selected, crate::providers::ApprovalAnswer::Once); - self.close_viewer(); - } else if close_clicked || !open { - self.close_viewer(); - } - ctx.request_repaint_after(std::time::Duration::from_millis(VIEWER_FRAME_MS)); - } - - /// Clicks, scrolls and keys on the picture go to the box while the person has control. - fn forward_input(&mut self, ui: &mut egui::Ui, resp: &egui::Response, frame: (f32, f32)) { - let Some(tx) = self.cu.input_tx.clone() else { return }; - let to_box = |pos: egui::Pos2| -> (i64, i64) { - let r = resp.rect; - (((pos.x - r.left()) / r.width() * frame.0).round() as i64, ((pos.y - r.top()) / r.height() * frame.1).round() as i64) - }; - if resp.clicked() || resp.secondary_clicked() || resp.double_clicked() { - resp.request_focus(); - } - if let Some(pos) = resp.interact_pointer_pos().or(resp.hover_pos()) { - let (x, y) = to_box(pos); - if resp.double_clicked() { - let _ = tx.send(Input::Click(x, y, "left", 2)); - } else if resp.clicked() { - let _ = tx.send(Input::Click(x, y, "left", 1)); - } else if resp.secondary_clicked() { - let _ = tx.send(Input::Click(x, y, "right", 1)); - } else if resp.hovered() { - let d = ui.input(|i| i.smooth_scroll_delta.y); - if d.abs() > 0.5 { - let _ = tx.send(Input::Scroll(x, y, d < 0.0, ((d.abs() / 40.0).ceil() as u32).max(1))); - } else if ui.input(|i| i.pointer.delta().length() > 3.0) { - let _ = tx.send(Input::Move(x, y)); - } - } - } - if !resp.has_focus() { - return; - } - ui.memory_mut(|m| m.set_focus_lock_filter(resp.id, egui::EventFilter { tab: true, horizontal_arrows: true, vertical_arrows: true, escape: true })); - let events = ui.input(|i| i.events.clone()); - for e in events { - match e { - egui::Event::Text(t) => { - let _ = tx.send(Input::Text(t)); - } - egui::Event::Paste(t) => { - let _ = tx.send(Input::Text(t)); - } - egui::Event::Key { key, pressed: true, modifiers, .. } => { - let name = match key { - Key::Enter => "Return", - Key::Tab => "Tab", - Key::Backspace => "BackSpace", - Key::Delete => "Delete", - Key::Escape => "Escape", - Key::ArrowUp => "Up", - Key::ArrowDown => "Down", - Key::ArrowLeft => "Left", - Key::ArrowRight => "Right", - Key::Home => "Home", - Key::End => "End", - Key::PageUp => "Prior", - Key::PageDown => "Next", - _ if modifiers.command || modifiers.ctrl || modifiers.alt => { - let n = key.name(); - if n.chars().count() == 1 { - n - } else { - continue; - } - } - _ => continue, - }; - let mut combo = vec![]; - if modifiers.command || modifiers.ctrl { - combo.push("ctrl".to_string()); - } - if modifiers.alt { - combo.push("alt".to_string()); - } - if modifiers.shift && (name.len() > 1 || !combo.is_empty()) { - combo.push("shift".to_string()); - } - combo.push(if name.len() == 1 { name.to_lowercase() } else { name.to_string() }); - let _ = tx.send(Input::Key(combo.join("+"))); - } - _ => {} - } + cloudbox::HELD_BY_PERSON.store(on, Ordering::SeqCst); } } - // ---------- the floating bar while a bot drives this Mac ---------- - pub(super) fn local_control_bar(&mut self, ctx: &egui::Context) { if self.demo { return; diff --git a/src/cloudbox.rs b/src/cloudbox.rs index 8d75f28..f19adf6 100644 --- a/src/cloudbox.rs +++ b/src/cloudbox.rs @@ -206,6 +206,16 @@ pub fn vnc_password() -> String { pw } +/// The password the box's VNC server really has: the one it was made with (its VNC_PASSWORD), so a +/// viewer signs in even when this AnyBot's settings were made anew; else this AnyBot's own. +pub fn box_vnc_password() -> String { + dk_timeout(&["exec", container(), "printenv", "VNC_PASSWORD"], 5_000) + .ok() + .map(|b| String::from_utf8_lossy(&b).trim().to_string()) + .filter(|p| !p.is_empty()) + .unwrap_or_else(vnc_password) +} + static BUSY: AtomicUsize = AtomicUsize::new(0); /// The person took control in the viewer: the bot's screen actions wait. pub static HELD_BY_PERSON: AtomicBool = AtomicBool::new(false); @@ -417,6 +427,13 @@ fn docker() -> Command { c } +/// A program in the box with its stdin and stdout piped to this side (as the bot user). +pub fn exec_piped(args: &[&str]) -> Result { + let mut a = vec!["exec", "-i", "-u", "bot", container()]; + a.extend_from_slice(args); + docker().args(&a).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().map_err(|e| format!("找不到 docker:{e}")) +} + /// The `docker …` prefix for shell strings (used by the command tools). fn docker_prefix() -> String { let host = docker_host(); @@ -1188,164 +1205,16 @@ pub fn thumbnail(width: u32) -> Option { image::open(&path).ok().map(|i| i.to_rgba8()) } -/// The screen now, for a small picture (no waking: None when the box isn't running): the live -/// picture when someone is watching, else one taken now. +/// The screen now, for a small picture (no waking: None when the box isn't running). People watch +/// it live through VNC (vnc.rs). pub fn frame() -> Option { if !run_state().is_running() { return None; } - let jpeg = match live_latest() { - Some(j) => j, - None => std::sync::Arc::new(dk_timeout(&["exec", container(), "import", "-window", "root", "-display", DISPLAY, "-quality", "80", "jpeg:-"], 5_000).ok()?), - }; + let jpeg = dk_timeout(&["exec", container(), "import", "-window", "root", "-display", DISPLAY, "-quality", "80", "jpeg:-"], 5_000).ok()?; image::load_from_memory(&jpeg).ok().map(|i| i.to_rgba8()) } -// ---------- the live picture (people watching) ---------- -// One lasting `docker exec` takes the desktop as a JPEG inside the box (ImageMagick, in C) a few -// times a second and sends a picture only when it changed; a viewer asks for "a picture newer -// than mine" and gets it the moment there is one. It used to be one exec per picture (a 1 MB -// PNG, decoded and encoded again as a JPEG here) and a fixed pause after each: about one -// picture a second, so a click showed its result a second or two later. It stops when nobody -// has asked for a while. - -/// JPEG quality of the live picture (text stays readable; 60–90 KB for 1280×800). -const LIVE_QUALITY: u8 = 70; -/// Pause between looks at the screen inside the box: short while it is changing (a click's -/// result, scrolling), longer while it stays still. -const LIVE_GAP_BUSY: &str = "0.04"; -const LIVE_GAP_STILL: &str = "0.12"; -/// Stop taking pictures when no viewer asked for this long. -const LIVE_IDLE: Duration = Duration::from_secs(10); - -struct Live { - seq: u64, - jpeg: std::sync::Arc>, - at: Instant, - asked: Instant, - running: bool, -} - -fn live() -> &'static (Mutex, std::sync::Condvar) { - static L: OnceLock<(Mutex, std::sync::Condvar)> = OnceLock::new(); - L.get_or_init(|| (Mutex::new(Live { seq: 0, jpeg: Default::default(), at: Instant::now(), asked: Instant::now(), running: false }), std::sync::Condvar::new())) -} - -/// The latest live picture, when one was taken in the last few seconds. -fn live_latest() -> Option>> { - let l = live().0.lock().unwrap_or_else(|e| e.into_inner()); - (l.running && l.seq > 0 && l.at.elapsed() < Duration::from_secs(3)).then(|| l.jpeg.clone()) -} - -/// A picture of the desktop newer than `after` (0: any), waiting up to `wait` for the screen to -/// change; the same one again when it didn't. (number, JPEG); None when the box isn't running. -pub fn live_frame(after: u64, wait: Duration) -> Option<(u64, std::sync::Arc>)> { - if !run_state().is_running() { - return None; - } - let (m, cv) = live(); - let mut l = m.lock().unwrap_or_else(|e| e.into_inner()); - l.asked = Instant::now(); - if !l.running { - l.running = true; - std::thread::spawn(live_loop); - } - let until = Instant::now() + wait; - while l.seq == 0 || l.seq <= after { - let left = until.saturating_duration_since(Instant::now()); - if left.is_zero() { - break; - } - l = cv.wait_timeout(l, left).map(|r| r.0).unwrap_or_else(|e| e.into_inner().0); - } - (l.seq > 0).then(|| (l.seq, l.jpeg.clone())) -} - -/// The box's side: a picture whenever the screen changed, as "\n", and "0\n" now -/// and then while it stays still. Ending the docker exec on this side doesn't end the loop in -/// the box (a still screen never writes, so it never noticed): it ends the one before it when -/// it starts, and itself when a write fails (the "0" lines make sure one does). -fn live_script() -> String { - format!( - "[ -f /tmp/anybot-live.pid ] && kill $(cat /tmp/anybot-live.pid) 2>/dev/null; echo $$ > /tmp/anybot-live.pid; f=/tmp/anybot-live-$$.jpg; trap 'rm -f $f' EXIT; prev=; still=0; \ - while :; do import -window root -display {DISPLAY} -quality {LIVE_QUALITY} jpeg:$f 2>/dev/null || exit 3; h=$(cksum < $f); \ - if [ \"$h\" != \"$prev\" ]; then prev=$h; still=0; {{ wc -c < $f && cat $f; }} || exit 0; sleep {LIVE_GAP_BUSY}; \ - else still=$((still+1)); if [ $still -ge 15 ]; then still=0; echo 0 || exit 0; fi; sleep {LIVE_GAP_STILL}; fi; done" - ) -} - -/// End the picture loop in the box (see live_script). -fn live_stop_in_box() { - let _ = dk_timeout(&["exec", container(), "sh", "-c", "[ -f /tmp/anybot-live.pid ] && kill $(cat /tmp/anybot-live.pid) 2>/dev/null; rm -f /tmp/anybot-live.pid; true"], 5_000); -} - -fn live_loop() { - use std::io::{BufRead, Read}; - let (m, cv) = live(); - let stop = |m: &Mutex| m.lock().unwrap_or_else(|e| e.into_inner()).running = false; - let Ok(mut child) = docker().args(["exec", "-u", "bot", container(), "sh", "-c", &live_script()]).stdout(Stdio::piped()).stderr(Stdio::null()).spawn() else { - return stop(m); - }; - let Some(out) = child.stdout.take() else { - let _ = child.kill(); - return stop(m); - }; - // Reading blocks while the screen stays the same; the watcher ends it when nobody asks. - let done = std::sync::Arc::new(AtomicBool::new(false)); - let d2 = done.clone(); - let reader = std::thread::spawn(move || { - let mut r = std::io::BufReader::new(out); - let mut line = String::new(); - while !d2.load(Ordering::SeqCst) { - line.clear(); - if r.read_line(&mut line).unwrap_or(0) == 0 { - break; - } - let Ok(n) = line.trim().parse::() else { break }; - // "0": still there, nothing new. - if n == 0 { - continue; - } - let mut jpeg = vec![0u8; n]; - if r.read_exact(&mut jpeg).is_err() { - break; - } - if !jpeg.starts_with(&[0xff, 0xd8]) { - continue; - } - let mut l = m.lock().unwrap_or_else(|e| e.into_inner()); - l.seq += 1; - l.jpeg = std::sync::Arc::new(jpeg); - l.at = Instant::now(); - cv.notify_all(); - } - d2.store(true, Ordering::SeqCst); - }); - loop { - std::thread::sleep(Duration::from_millis(500)); - let idle = m.lock().unwrap_or_else(|e| e.into_inner()).asked.elapsed() > LIVE_IDLE; - if idle || done.load(Ordering::SeqCst) || !run_state().is_running() { - break; - } - } - done.store(true, Ordering::SeqCst); - let _ = child.kill(); - let _ = child.wait(); - if run_state().is_running() { - live_stop_in_box(); - } - let _ = reader.join(); - let mut l = m.lock().unwrap_or_else(|e| e.into_inner()); - l.running = false; - // Viewers waiting on a picture that won't come get the last one now. - cv.notify_all(); -} - -/// The desktop's size (for a viewer mapping its taps), when known. -pub fn screen_size() -> Option<(u32, u32)> { - box_size().ok() -} - /// The desktop's size in pixels (asked once per box). fn box_size() -> Result<(u32, u32), String> { if let Some(s) = shared().lock().unwrap_or_else(|e| e.into_inner()).size { @@ -1605,8 +1474,8 @@ pub fn computer_run(name: &str, v: &Value) -> Result<(String, Option), S } _ => {} } - if HELD_BY_PERSON.load(Ordering::SeqCst) { - return Err("使用者正在自己操作這台電腦(已取得控制權)。請等他交還控制權後再操作,或先截圖看看畫面。".into()); + if HELD_BY_PERSON.load(Ordering::SeqCst) || crate::vnc::person_active() { + return Err("使用者正在自己操作這台電腦。請等他操作完(停下幾秒後)再繼續,或先截圖看看畫面。".into()); } let (note, picture) = if name == "computer_use_open_app" { let note = open_on_box(v["app"].as_str().unwrap_or(""))?; @@ -1623,85 +1492,13 @@ pub fn computer_run(name: &str, v: &Value) -> Result<(String, Option), S } } -// ---------- the viewer's own input (when the person takes control) ---------- -// Same scripts, at the desktop's own pixels (the viewer shows it full size), no picture back, -// through one lasting shell in the box (a click or a key no longer starts a docker exec of its -// own: 50–200 ms each, more with a Docker host elsewhere). Typed text rides along as base64. - -fn input_shell() -> &'static Mutex> { - static S: OnceLock>> = OnceLock::new(); - S.get_or_init(|| Mutex::new(None)) -} - -/// One line for the lasting shell: the step's script, its text (if any) fed on its stdin. -pub fn input_line(script: &str, stdin: Option<&str>) -> String { - let script = script.replace('\n', " "); - match stdin { - Some(t) => format!("printf %s {} | base64 -d | {{ {script}; }}\n", base64::engine::general_purpose::STANDARD.encode(t)), - None => format!("{{ {script}; }} ) { - use std::io::Write; - let line = input_line(script, stdin); - let mut sh = input_shell().lock().unwrap_or_else(|e| e.into_inner()); - for _ in 0..2 { - let alive = sh.as_mut().map(|c| matches!(c.try_wait(), Ok(None))).unwrap_or(false); - if !alive { - *sh = docker() - .args(["exec", "-i", "-u", "bot", "-e", "DISPLAY=:1", container(), "sh", "-c", "while IFS= read -r l; do eval \"$l\" >/dev/null 2>&1; done"]) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .ok(); - } - let Some(c) = sh.as_mut() else { return }; - if c.stdin.as_mut().map(|i| i.write_all(line.as_bytes()).and_then(|_| i.flush()).is_ok()).unwrap_or(false) { - return; - } - // The box restarted under it: start a new one and try once more. - let _ = c.kill(); - let _ = c.wait(); - *sh = None; - } -} - -fn viewer(name: &str, v: Value) { - if let Ok((script, stdin, _)) = box_script(name, &v, (1.0, 1.0)) { - send_input(&script, stdin.as_deref()); - } - touch(); -} - -pub fn viewer_click(x: i64, y: i64, button: &str, count: u32) { - viewer("computer_use_click", serde_json::json!({"x": x, "y": y, "button": button, "count": count.clamp(1, 3)})); -} - -pub fn viewer_move(x: i64, y: i64) { - viewer("computer_use_mouse_move", serde_json::json!({"x": x, "y": y})); -} - -pub fn viewer_scroll(x: i64, y: i64, down: bool, steps: u32) { - viewer("computer_use_scroll", serde_json::json!({"x": x, "y": y, "direction": if down { "down" } else { "up" }, "amount": steps.clamp(1, 20)})); -} - -/// A key from a viewer: an X key name (「BackSpace」「Return」, ctrl+c…) or the bot's names. -pub fn viewer_key(combo: &str) { - let c = combo.trim(); - if c.is_empty() || !c.chars().all(|ch| ch.is_ascii_alphanumeric() || "+_-".contains(ch)) { - return; - } - // X key names pass straight through (they are what the viewers send). - let x = combo_to_x(c).unwrap_or_else(|_| c.to_string()); - send_input(&format!("xdotool key --clearmodifiers {x}"), None); - touch(); -} +// ---------- text a person types in a viewer ---------- +/// Text from a person's viewer (any language): typed or pasted into the window in front, like +/// the bot's typing. (Their mouse and keys go through VNC itself.) pub fn viewer_text(text: &str) { if !text.is_empty() { - send_input(&type_script(text), Some(text)); + let _ = box_exec(&type_script(text), Some(text), false); } touch(); } @@ -1876,65 +1673,8 @@ mod tests { assert!(box_script("computer_use_scroll", &serde_json::json!({"direction": "sideways"}), s).is_err()); } - #[test] - fn viewer_input_is_one_line() { - // A click: its script on one line, no stdin. - assert_eq!(input_line("xdotool mousemove 1 2 sleep 0.03 click --repeat 1 --delay 80 1", None), "{ xdotool mousemove 1 2 sleep 0.03 click --repeat 1 --delay 80 1; } seq && react < Duration::from_millis(600), "{react:?}"); - // A screen that keeps changing: pictures a second. - let _ = box_exec("(setsid xterm -geometry 60x20+40+40 -e sh -c 'while :; do date +%s.%N; done' >/dev/null 2>&1 &)", None, false); - std::thread::sleep(Duration::from_millis(1200)); - let (mut n, _) = live_frame(0, Duration::from_secs(2)).unwrap(); - let (start, t) = (n, Instant::now()); - while t.elapsed() < Duration::from_secs(3) { - n = live_frame(n, Duration::from_secs(2)).unwrap().0; - } - let fps = (n - start) as f64 / t.elapsed().as_secs_f64(); - println!("changing screen: {fps:.1} pictures a second"); - // (pkill straight from docker exec: inside `sh -c` it matched its own shell first.) - let _ = dk(&["exec", container(), "pkill", "-f", "date +%s.%N"]); - assert!(fps >= 4.0, "{fps}"); - // Nobody asks: it stops, in the box too. - std::thread::sleep(LIVE_IDLE + Duration::from_secs(2)); - assert!(!live().0.lock().unwrap().running); - let left = dk(&["exec", container(), "sh", "-c", "ps -eo args | grep -c '[a]nybot-live' || true"]).unwrap_or_default(); - assert_eq!(left.trim(), "0", "picture loops left in the box"); - } - /// Uses the real Docker: `cargo test --release -- --ignored box_screen_is_exact --nocapture`. - /// The bot's clicks land where its picture says, a phone's tap where the phone's picture says, - /// the same spot twice doesn't hang, typing (ASCII, Chinese, a key by name) reaches the app, + /// The bot's clicks land where its picture says, the same spot twice doesn't hang, typing (ASCII, Chinese, a key by name) reaches the app, /// and every step comes back with the screen after it. #[test] #[ignore] @@ -1960,9 +1700,6 @@ mod tests { println!("two clicks with pictures: {} ms", t.elapsed().as_millis()); assert!(t.elapsed() < Duration::from_secs(6)); assert_eq!(pointer(), ((40.0 * w as f64 / fw as f64).round() as i64, (40.0 * h as f64 / fh as f64).round() as i64)); - // A phone's tap is in the full-size frame it was sent. - viewer_click(321, 123, "left", 1); - assert_eq!(pointer(), (321, 123)); // Typing reaches the app in front: a fresh terminal, ASCII then Chinese, a key by name, return. let _ = dk(&["exec", "-u", "bot", container(), "rm", "-f", "/home/bot/exact.txt"]); let (txt, img) = computer_run("computer_use_open_app", &serde_json::json!({"app": "終端機"})).unwrap(); diff --git a/src/computer.rs b/src/computer.rs index 404c165..2044a7a 100644 --- a/src/computer.rs +++ b/src/computer.rs @@ -536,13 +536,29 @@ fn open_app(app: &str) -> Result { if app.is_empty() { return Err("請給要打開的 App 名稱".into()); } - crate::desktop::launch(app).or_else(|e| match app_named(app) { - Some(real) if real != app => crate::desktop::launch(&real), + launch(app).or_else(|e| match app_named(app) { + Some(real) if real != app => launch(&real), _ => Err(e), })?; Ok(format!("已打開「{app}」,放在最前面。")) } +/// Open an app or bring it forward. macOS: LaunchServices (`open -a`), which needs no +/// permission (agent-desktop's launch waited on the window through accessibility and failed +/// without 輔助使用 now and then); elsewhere agent-desktop's. +fn launch(app: &str) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + let out = std::process::Command::new("open").args(["-a", app]).output().map_err(|e| e.to_string())?; + if out.status.success() { + return Ok(()); + } + Err(format!("找不到「{app}」這個 App")) + } + #[cfg(not(target_os = "macos"))] + crate::desktop::launch(app) +} + /// Bring the app a control belongs to in front, so a click at its spot lands in it (and not in /// a window lying over it). Nothing to do when it is in front already. fn bring_forward(app: &str) { @@ -555,7 +571,7 @@ fn bring_forward(app: &str) { if app.is_empty() || front.eq_ignore_ascii_case(app) { return; } - if crate::desktop::launch(app).is_ok() { + if launch(app).is_ok() { std::thread::sleep(std::time::Duration::from_millis(300)); } } diff --git a/src/desktop.rs b/src/desktop.rs index 02217a4..b5f3b35 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -259,7 +259,8 @@ pub fn read_app(app: &str) -> Result<(String, Vec), String> { Ok((app_name, lines)) } -/// Open an app, or bring it to the front when it runs. +/// Open an app, or bring it to the front when it runs (macOS uses LaunchServices instead). +#[cfg(not(target_os = "macos"))] pub fn launch(app: &str) -> Result<(), String> { run("launch", &serde_json::json!({"app": app})).map(|_| ()) } diff --git a/src/main.rs b/src/main.rs index 654c1e3..fdcab69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,7 @@ mod tools; mod triggers; mod vault; mod vault_store; +mod vnc; mod voice; mod tunnel; mod web; diff --git a/src/remote.rs b/src/remote.rs index 2e40d56..6b3c4b2 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -322,6 +322,8 @@ pub fn handle(req: Req, via_relay: bool) -> Resp { let path: Vec<&str> = parts.iter().map(|s| *s).collect(); match (req.method.as_str(), path.as_slice()) { ("GET", []) | ("GET", ["index.html"]) => return page(), + ("GET", ["screen"]) => return Resp::bytes("text/html; charset=utf-8", SCREEN_PAGE.as_bytes().to_vec()).with_header("Content-Security-Policy", "default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:"), + ("GET", ["novnc.js"]) => return Resp::bytes("text/javascript; charset=utf-8", NOVNC.as_bytes().to_vec()).with_header("Cache-Control", "public, max-age=86400"), ("GET", ["manifest.webmanifest"]) => return Resp::bytes("application/manifest+json", MANIFEST.as_bytes().to_vec()), ("GET", ["icon.svg"]) => return Resp::bytes("image/svg+xml", ICON.as_bytes().to_vec()), ("GET", [name]) if ICON_PNGS.iter().any(|(n, _)| n == name) => return Resp::bytes("image/png", ICON_PNGS.iter().find(|(n, _)| n == name).map(|(_, b)| b.to_vec()).unwrap_or_default()), @@ -415,7 +417,7 @@ pub fn handle(req: Req, via_relay: bool) -> Resp { hub().push(cmd); Resp::json(&json!({"ok": true})) } - ("GET", ["api", "screen", which]) => screen(which, &s, req.query.get("after").and_then(|a| a.parse().ok())), + ("GET", ["api", "screen", which]) => screen(which, &s), ("POST", ["api", "screen", which]) => screen_input(which, &req.json(), &s), ("POST", ["api", "device", "run"]) => device_run(&req.json(), &s, via_relay, req.header("x-anybot-from")), ("GET", ["api", "sync"]) => { @@ -435,24 +437,16 @@ pub fn handle(req: Req, via_relay: bool) -> Resp { Err(e) => Resp::error(503, &e), }, ("POST", ["api", "sync", "upload", id]) => upload(id.parse().unwrap_or(0), &req), - // A screen of this brain watches (and may take over) the bots' own computer, which lives - // here: its picture on that screen used to be its own computer's, not this one's. - ("GET", ["api", "sync", "screen"]) => { - if req.query.get("thumb").map(|t| t == "1").unwrap_or(false) { - match crate::cloudbox::thumbnail(320) { - Some(img) => { - let rgb = image::DynamicImage::ImageRgba8(img).to_rgb8(); - match crate::computer::encode_jpeg(&rgb) { - Some(j) => Resp::bytes("image/jpeg", j), - None => Resp::error(500, "畫面編碼失敗"), - } - } - None => Resp::error(404, "還沒有畫面"), - } - } else { - screen("box", &s, req.query.get("after").and_then(|a| a.parse().ok())) - } - } + // A screen of this brain shows the bots' own computer, which lives here: a small picture + // (people watch it live through VNC, see vnc.rs). + ("GET", ["api", "sync", "screen"]) => match crate::cloudbox::thumbnail(320) { + Some(img) => match crate::computer::encode_jpeg(&image::DynamicImage::ImageRgba8(img).to_rgb8()) { + Some(j) => Resp::bytes("image/jpeg", j), + None => Resp::error(500, "畫面編碼失敗"), + }, + None => Resp::error(404, "還沒有畫面"), + }, + // 接管 / 我完成了 from a screen: the bot's steps wait (or not) for the person. ("POST", ["api", "sync", "screen"]) => { let v = req.json(); match v["kind"].as_str().unwrap_or("") { @@ -460,25 +454,11 @@ pub fn handle(req: Req, via_relay: bool) -> Resp { crate::cloudbox::HELD_BY_PERSON.store(v["on"].as_bool().unwrap_or(false), Ordering::SeqCst); Resp::json(&json!({"ok": true})) } - "viewer" => { - crate::cloudbox::set_viewer_open(v["on"].as_bool().unwrap_or(false)); - Resp::json(&json!({"ok": true})) - } - // The person's own hands on the picture (frame pixels, like this computer's own - // viewer; not the bot's tools, which stop while the person holds it). - "click" | "move" | "scroll" | "key" | "text" => { - crate::cloudbox::touch(); - let (x, y) = (v["x"].as_i64().unwrap_or(0), v["y"].as_i64().unwrap_or(0)); - match v["kind"].as_str().unwrap_or("") { - "click" => crate::cloudbox::viewer_click(x, y, if v["button"].as_str() == Some("right") { "right" } else { "left" }, v["count"].as_u64().unwrap_or(1).clamp(1, 3) as u32), - "move" => crate::cloudbox::viewer_move(x, y), - "scroll" => crate::cloudbox::viewer_scroll(x, y, v["down"].as_bool().unwrap_or(true), v["steps"].as_u64().unwrap_or(1).clamp(1, 20) as u32), - "key" => crate::cloudbox::viewer_key(v["key"].as_str().unwrap_or("")), - _ => crate::cloudbox::viewer_text(v["text"].as_str().unwrap_or("")), - } - Resp::json(&json!({"ok": true})) - } - _ => screen_input("box", &v, &s), + "wake" => match crate::cloudbox::ensure_running() { + Ok(()) => Resp::json(&json!({"ok": true})), + Err(e) => Resp::error(500, &e), + }, + _ => Resp::error(400, "不認得的操作"), } } ("POST", ["api", "sync", "models"]) => match hub().ask_sync_for(SyncKind::Models(req.json()["provider"].as_str().unwrap_or("").to_string()), 45) { @@ -606,27 +586,10 @@ fn upload(bot: u64, req: &Req) -> Resp { } /// A frame of the bot's computer ("box") or this Mac ("mac") as JPEG. -/// How long a viewer's request for a newer picture waits for the screen to change (well under -/// the account server's forward timeout). -const LIVE_WAIT_MS: u64 = 8_000; - -/// A screen's picture for a viewer. The box: its live picture, as it was taken (no decoding and -/// encoding again here); with `after` (the number of the picture the viewer has) it answers the -/// moment the screen changes, or with the same picture after a few seconds. X-Seq is its number, -/// X-Size the desktop's size (what the viewer's taps are in). -fn screen(which: &str, s: &Settings, after: Option) -> Resp { +/// This Mac's screen for the phone (when allowed). The bots' computer is watched through VNC +/// (the page /screen, vnc.rs). +fn screen(which: &str, s: &Settings) -> Resp { match which { - "box" => { - crate::cloudbox::touch(); - let wait = Duration::from_millis(if after.is_some() { LIVE_WAIT_MS } else { 3_000 }); - match crate::cloudbox::live_frame(after.unwrap_or(0), wait) { - Some((seq, jpeg)) => { - let (w, h) = crate::cloudbox::screen_size().unwrap_or_default(); - Resp::bytes("image/jpeg", jpeg.to_vec()).with_header("X-Size", &format!("{w}x{h}")).with_header("X-Seq", &seq.to_string()) - } - None => Resp::error(503, "Bot 的電腦在休息,按「叫醒」"), - } - } "mac" if s.mac_from_phone => { let (text, ok, img) = crate::computer::run_by("computer_use_screenshot", "{}", "手機"); match (ok, img) { @@ -644,41 +607,15 @@ fn screen(which: &str, s: &Settings, after: Option) -> Resp { } } -/// The phone taps, types or scrolls on a screen, as the person, in the pixels of the picture it -/// was sent: the box's frame is its desktop at full size, this computer's is the bot-sized -/// picture (shot_size). It used to go through the bot's tools, which mapped the tap with the -/// bot's last screenshot (a different size: taps landed a quarter off, or failed with "take a -/// screenshot first") and refused it while the person held the screen. +/// The phone taps, types or scrolls on this Mac's screen, as the person, in the pixels of the +/// picture it was sent (shot_size). The bots' computer takes the person's input through VNC. fn screen_input(which: &str, v: &Value, s: &Settings) -> Resp { - let kind = v["kind"].as_str().unwrap_or(""); - if which == "box" && kind == "wake" { - return match crate::cloudbox::ensure_running() { - Ok(()) => Resp::json(&json!({"ok": true})), - Err(e) => Resp::error(500, &e), - }; - } let (x, y) = (v["x"].as_f64().unwrap_or(0.0), v["y"].as_f64().unwrap_or(0.0)); let down = v["direction"].as_str().unwrap_or("down") != "up"; let amount = v["amount"].as_i64().unwrap_or(3).clamp(1, 20); match which { - "box" => { - if !matches!(kind, "click" | "type" | "key" | "scroll") { - return Resp::error(400, "不認得的操作"); - } - if let Err(e) = crate::cloudbox::ensure_running() { - return Resp::error(500, &e); - } - let (xi, yi) = (x.round() as i64, y.round() as i64); - match kind { - "click" => crate::cloudbox::viewer_click(xi, yi, if v["button"].as_str() == Some("right") { "right" } else { "left" }, v["count"].as_u64().unwrap_or(1) as u32), - "type" => crate::cloudbox::viewer_text(v["text"].as_str().unwrap_or("")), - "key" => crate::cloudbox::viewer_key(v["key"].as_str().unwrap_or("")), - _ => crate::cloudbox::viewer_scroll(xi, yi, down, amount as u32), - } - Resp::json(&json!({"ok": true})) - } "mac" if s.mac_from_phone => { - let (name, args) = match kind { + let (name, args) = match v["kind"].as_str().unwrap_or("") { "click" => ("computer_use_click", json!({"x": x, "y": y, "button": v["button"].as_str().unwrap_or("left"), "count": v["count"].as_u64().unwrap_or(1)})), "type" => ("computer_use_typing", json!({"value": v["text"]})), "key" => ("computer_use_press_key", json!({"key": v["key"]})), @@ -690,7 +627,8 @@ fn screen_input(which: &str, v: &Value, s: &Settings) -> Resp { Err(e) => Resp::error(500, &e), } } - _ => Resp::error(403, "這台 Mac 沒有開放從手機操作"), + "mac" => Resp::error(403, "這台 Mac 沒有開放從手機操作"), + _ => Resp::error(404, "沒有這個畫面"), } } @@ -868,10 +806,69 @@ pub fn set_lan(on: bool) -> Result { .ok_or("找不到可用的連接埠")?; let port = listener.local_addr().map_err(|e| e.to_string())?.port(); *LAN_BOUND.lock().unwrap() = Some(port); - std::thread::spawn(move || httpd::serve(listener, Arc::new(|r| handle(r, false)), Arc::new(move || LAN_GEN.load(Ordering::SeqCst) == gen))); + std::thread::spawn(move || httpd::serve_upgrade(listener, Arc::new(|r| handle(r, false)), Some(Arc::new(lan_vnc)), Arc::new(move || LAN_GEN.load(Ordering::SeqCst) == gen))); Ok(port) } +/// The phone page on the local network watching the bots' computer: /api/vnc, signed in with +/// the phone's password like every other request here; the VNC stream goes straight to the box. +fn lan_vnc(req: Req, mut c: std::net::TcpStream) { + use std::io::Write; + use tungstenite::{protocol::Role, Message, WebSocket}; + if req.path != "/api/vnc" { + return httpd::write_response(&mut c, &Resp::error(404, "沒有這個網址")); + } + if devices_ok(&req.bearer()).is_none() { + return httpd::write_response(&mut c, &Resp::error(401, "請先登入")); + } + let key = req.header("sec-websocket-key").to_string(); + let accept = tungstenite::handshake::derive_accept_key(key.as_bytes()); + if key.is_empty() || c.write_all(format!("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {accept}\r\n\r\n").as_bytes()).is_err() { + return; + } + let _ = c.set_nodelay(true); + let _ = c.set_read_timeout(Some(Duration::from_millis(2))); + let mut ws = WebSocket::from_raw_socket(c, Role::Server, None); + let (tx, rx) = std::sync::mpsc::channel::>(); + let send = move |b: &[u8]| tx.send(b.to_vec()).is_ok(); + let Ok(mut session) = crate::vnc::open(send.clone()) else { return }; + if !send(crate::vnc::GREETING) { + return; + } + 'live: loop { + match ws.read() { + Ok(Message::Binary(b)) => { + if !session.feed(&b, &send) { + break; + } + } + Ok(Message::Text(t)) => crate::vnc::person_typed(&t), + Ok(Message::Close(_)) => break, + Ok(_) => {} + Err(tungstenite::Error::Io(e)) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {} + Err(_) => break, + } + loop { + match rx.try_recv() { + // An empty send: the box's side ended. + Ok(b) if b.is_empty() => break 'live, + Ok(b) => { + if ws.write(Message::Binary(b.into())).is_err() { + break 'live; + } + } + Err(_) => break, + } + } + match ws.flush() { + Err(tungstenite::Error::Io(e)) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {} + Err(_) => break, + Ok(()) => {} + } + } + let _ = ws.close(None); +} + /// Addresses a phone can open (local network and Tailscale). pub fn addresses(port: u16) -> Vec { // macOS: ifconfig ("inet 192.168.1.5 netmask…"); Linux: `ip -4 -o addr` ("… inet 192.168.1.5/24 …"). @@ -1212,16 +1209,11 @@ fn to_brain(s: &Settings, method: &str, rest: &str, query: &[(&str, String)], bo } fn to_brain_raw(s: &Settings, method: &str, rest: &str, query: &[(&str, String)], body: &[u8], secs: u64) -> Result<(u16, Vec), String> { - to_brain_full(s, method, rest, query, body, secs).map(|(st, b, _)| (st, b)) -} - -/// Same, with the answer's headers. -fn to_brain_full(s: &Settings, method: &str, rest: &str, query: &[(&str, String)], body: &[u8], secs: u64) -> Result<(u16, Vec, Vec<(String, String)>), String> { if s.brain_id.is_empty() { return Err("沒有選大腦".into()); } if let Some(r) = crate::tunnel::call(&s.brain_id, method, &format!("/api/sync{rest}"), query, body, secs) { - return r.map(|a| (a.status, a.body, a.headers)); + return r.map(|a| (a.status, a.body)); } let q: Vec = query.iter().map(|(k, v)| format!("{k}={}", url_escape(v))).collect(); let url = brain_url(s, &format!("{rest}{}{}", if q.is_empty() { "" } else { "?" }, q.join("&")))?; @@ -1229,9 +1221,8 @@ fn to_brain_full(s: &Settings, method: &str, rest: &str, query: &[(&str, String) let r = if method == "GET" { agent(secs).get(&url).header("Authorization", &auth).call() } else { agent(secs).post(&url).header("Authorization", &auth).header("Content-Type", "application/octet-stream").send(body) }; let mut resp = r.map_err(|e| format!("連不到帳號伺服器:{}", why_unreachable(&e)))?; let st = resp.status().as_u16(); - let headers = resp.headers().iter().filter_map(|(k, v)| Some((k.as_str().to_string(), v.to_str().ok()?.to_string()))).collect(); let bytes = resp.body_mut().with_config().limit(200 * 1024 * 1024).read_to_vec().map_err(|_| "和大腦的連線到一半斷掉了".to_string())?; - Ok((st, bytes, headers)) + Ok((st, bytes)) } /// What changed at the brain since `since` (waits up to LONG_POLL_SECS when nothing has). @@ -1259,15 +1250,21 @@ pub fn sync_screen(s: &Settings, thumb: bool) -> Result, String> { Ok(bytes) } -/// The brain's bots' computer, live: a picture newer than number `after` (or the same one when -/// the screen didn't change for a few seconds), with its number. -pub fn sync_live(s: &Settings, after: u64) -> Result<(u64, Vec), String> { - let (st, bytes, headers) = to_brain_full(s, "GET", "/screen", &[("after", after.to_string())], &[], LIVE_WAIT_MS / 1000 + 15)?; - if !(200..300).contains(&st) { - return Err(why_refused(st, &serde_json::from_slice(&bytes).unwrap_or(Value::Null), "大腦")); +/// A link for this account's browser to watch the brain's computer (/screen on the account +/// server; one-time, it works for ten minutes). +pub fn screen_ticket(s: &Settings) -> Result { + if s.relay_url.is_empty() || s.brain_id.is_empty() { + return Err("這台沒有連到帳號的大腦".into()); } - let seq = headers.iter().find(|(k, _)| k.eq_ignore_ascii_case("x-seq")).and_then(|(_, v)| v.parse().ok()).unwrap_or(0); - Ok((seq, bytes)) + let url = format!("{}/relay/screen-ticket", s.relay_url.trim_end_matches('/')); + let mut r = agent(15) + .post(&url) + .header("Authorization", &format!("Bearer {}", s.host_token)) + .header("Content-Type", "application/json") + .send(json!({"host": s.brain_id}).to_string().as_bytes()) + .map_err(|e| format!("連不到帳號伺服器:{}", why_unreachable(&e)))?; + let v: Value = r.body_mut().read_json().map_err(|_| "帳號伺服器的回答看不懂".to_string())?; + v["ticket"].as_str().map(String::from).ok_or_else(|| "帳號伺服器沒有給看畫面的連結(它可能是舊版,請更新)".into()) } /// A file the brain keeps (an attachment shown in a chat), as bytes. @@ -1526,6 +1523,9 @@ pub fn devices_online(s: &Settings) -> Vec<(String, String, bool)> { // ---------- the phone page ---------- const PAGE: &str = include_str!("../mobile/index.html"); +/// The bots' computer, watched and used in a browser (noVNC; see vnc.rs). +const SCREEN_PAGE: &str = include_str!("../mobile/screen.html"); +const NOVNC: &str = include_str!("../mobile/novnc.js"); pub const MANIFEST: &str = r##"{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"},{"src":"icon-192.png","sizes":"192x192","type":"image/png","purpose":"any maskable"},{"src":"icon-512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"}]}"##; /// The app's icon, the same drawing everywhere (icons/anybot.svg is the Mac/Linux one). pub const ICON: &str = include_str!("../mobile/icon.svg"); diff --git a/src/tunnel.rs b/src/tunnel.rs index 0aafda1..619b2e4 100644 --- a/src/tunnel.rs +++ b/src/tunnel.rs @@ -26,7 +26,6 @@ const CUT: &str = "和帳號伺服器的連線斷了,正在重新連線"; pub struct Answer { pub status: u16, pub body: Vec, - pub headers: Vec<(String, String)>, } type Calls = Arc>>>>; @@ -229,8 +228,15 @@ pub fn run(url: &str, token: &str, alive: &dyn Fn() -> bool, status: &dyn Fn(&st true } +/// How long a turn of the connection's loop waits for something to read before sending what is +/// queued: short while things are moving (a live screen), longer when quiet. A fixed 40 ms +/// added up to 40 ms to every message sent, on each side. +const READ_WAIT_BUSY: Duration = Duration::from_millis(2); +const READ_WAIT_QUIET: Duration = Duration::from_millis(40); + fn keep(mut ws: WebSocket, serial: u64, alive: &dyn Fn() -> bool) { - let _ = ws.get_ref().tcp().set_read_timeout(Some(Duration::from_millis(40))); + let _ = ws.get_ref().tcp().set_read_timeout(Some(READ_WAIT_QUIET)); + let (mut moved, mut busy) = (Instant::now() - Duration::from_secs(60), false); let (tx, rx) = channel::>(); let calls: Calls = Arc::default(); *link().lock().unwrap() = Some(Link { tx: tx.clone(), calls: calls.clone(), serial }); @@ -238,9 +244,15 @@ fn keep(mut ws: WebSocket, serial: u64, alive: &dyn Fn() -> bool) { let _ = tx.send(wire::pack(&json!({"t": "hello", "brain": s.brain_id, "hands": s.device_link}), &[])); let (mut heard, mut pinged) = (Instant::now(), Instant::now()); 'live: while alive() { + let now_busy = moved.elapsed() < Duration::from_secs(2); + if now_busy != busy { + busy = now_busy; + let _ = ws.get_ref().tcp().set_read_timeout(Some(if busy { READ_WAIT_BUSY } else { READ_WAIT_QUIET })); + } match ws.read() { Ok(Message::Binary(b)) => { heard = Instant::now(); + moved = heard; if let Some((head, body)) = wire::unpack(&b) { // The relay says this machine was unregistered (signed out): stop for good. if head["t"] == "bye" { @@ -255,6 +267,7 @@ fn keep(mut ws: WebSocket, serial: u64, alive: &dyn Fn() -> bool) { Err(_) => break, } while let Ok(f) = rx.try_recv() { + moved = Instant::now(); if ws.write(Message::Binary(f.into())).is_err() { break 'live; } @@ -280,14 +293,72 @@ fn keep(mut ws: WebSocket, serial: u64, alive: &dyn Fn() -> bool) { *l = None; } drop(l); + streams().lock().unwrap().clear(); // Whoever waits on an answer hears now, not after a timeout. for (_, w) in calls.lock().unwrap().drain() { let _ = w.send(Err(CUT.into())); } } +/// People watching the bots' computer through the account server: one VNC stream each, by the +/// relay's stream id (vopen / vdata / vtext / vclose). +fn streams() -> &'static Mutex> { + static S: OnceLock>> = OnceLock::new(); + S.get_or_init(Default::default) +} + +fn on_stream(kind: &str, sid: String, body: Vec, tx: &Sender>) { + let send = { + let (tx, sid) = (tx.clone(), sid.clone()); + move |b: &[u8]| tx.send(wire::pack(&json!({"t": "vdata", "sid": sid}), b)).is_ok() + }; + let close = { + let (tx, sid) = (tx.clone(), sid.clone()); + move |why: &str| { + streams().lock().unwrap().remove(&sid); + let _ = tx.send(wire::pack(&json!({"t": "vclose", "sid": sid, "why": why}), &[])); + } + }; + match kind { + "vopen" => { + std::thread::spawn(move || { + let out = { + let (send, close) = (send.clone(), close.clone()); + move |b: &[u8]| if b.is_empty() { close("這台電腦的畫面關了"); false } else { send(b) } + }; + match crate::vnc::open(out) { + Ok(s) => { + streams().lock().unwrap().insert(sid, s); + // Only now: the viewer answers this at once. + if !send(crate::vnc::GREETING) { + close(""); + } + } + Err(e) => close(&e), + } + }); + } + "vdata" => { + let mut all = streams().lock().unwrap(); + let ok = all.get_mut(&sid).map(|s| s.feed(&body, &send)).unwrap_or(false); + drop(all); + if !ok { + close("這台電腦的畫面關了"); + } + } + "vtext" => crate::vnc::person_typed(&String::from_utf8_lossy(&body)), + _ => { + streams().lock().unwrap().remove(&sid); + } + } +} + fn on_frame(head: Value, body: Vec, tx: &Sender>, calls: &Calls) { - match head["t"].as_str().unwrap_or("") { + let t = head["t"].as_str().unwrap_or(""); + if matches!(t, "vopen" | "vdata" | "vtext" | "vclose") { + return on_stream(t, head["sid"].as_str().unwrap_or("").to_string(), body, tx); + } + match t { "job" => { let tx = tx.clone(); std::thread::spawn(move || { @@ -310,8 +381,7 @@ fn on_frame(head: Value, body: Vec, tx: &Sender>, calls: &Calls) { "ret" => { if let Some(w) = calls.lock().unwrap().remove(head["id"].as_str().unwrap_or("")) { let body = if head["z"].as_bool().unwrap_or(false) { unzip(&body) } else { Ok(body) }; - let headers = head["headers"].as_array().into_iter().flatten().filter_map(|x| Some((x[0].as_str()?.to_string(), x[1].as_str()?.to_string()))).collect(); - let _ = w.send(body.map(|body| Answer { status: head["status"].as_u64().unwrap_or(502) as u16, body, headers })); + let _ = w.send(body.map(|body| Answer { status: head["status"].as_u64().unwrap_or(502) as u16, body })); } } _ => {} diff --git a/src/vnc.rs b/src/vnc.rs new file mode 100644 index 0000000..a2cefb9 --- /dev/null +++ b/src/vnc.rs @@ -0,0 +1,263 @@ +// People watching and helping on the bots' computer: VNC, carried to them as a stream. +// +// The box already runs a VNC server (x11vnc, inside the container, with a password). A person's +// viewer (noVNC in a browser: the phone, or any computer through the account server) speaks VNC +// to this proxy, which reaches the box's server through one `docker exec` (a few lines of +// Python in the box joining its stdin/stdout to 127.0.0.1:5900). VNC sends only what changed on +// the screen and every mouse move and key as it happens, so watching is live and helping is +// like sitting at it — unlike pictures sent one by one. +// +// The proxy signs in to the box with its password itself and offers the viewer "no password", +// so the password never leaves this AnyBot; whoever reaches the proxy was already let in (an +// account session or one-time ticket at the account server, or the phone's password here). +// While the person clicks or types, the bot's own steps wait (HELD_BY_PERSON), and go on a +// little after they stop (person_active). + +use std::io::{Read, Write}; +use std::process::{Child, ChildStdin}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +/// The bot waits this long after the person's last click or key before it goes on. +const HELD_AFTER_INPUT: Duration = Duration::from_secs(8); + +/// Joins the box's VNC server to stdin/stdout, and ends when either side does (a loop left in +/// the box would hold the connection open). +const BRIDGE: &str = r#"import os,select,socket +s=socket.create_connection(('127.0.0.1',5900)) +while True: + r=select.select([s,0],[],[])[0] + if s in r: + d=s.recv(65536) + if not d: break + os.write(1,d) + if 0 in r: + d=os.read(0,65536) + if not d: break + s.sendall(d) +"#; + +/// Viewers connected now (the box stays awake while there is one). +static OPEN: AtomicUsize = AtomicUsize::new(0); +/// When the person last clicked or typed through a viewer. +static LAST_INPUT: Mutex> = Mutex::new(None); + +/// VNC authentication: the 16-byte challenge encrypted with DES, the key being the password's +/// first 8 bytes with each byte's bits reversed (RFC 6143 §7.2.2, as every VNC server does it). +pub fn vnc_response(password: &str, challenge: &[u8; 16]) -> [u8; 16] { + use des::cipher::{BlockEncrypt, KeyInit}; + let mut key = [0u8; 8]; + for (k, b) in key.iter_mut().zip(password.bytes()) { + *k = b.reverse_bits(); + } + let c = des::Des::new_from_slice(&key).expect("8-byte key"); + let mut out = *challenge; + for block in out.chunks_mut(8) { + c.encrypt_block(des::cipher::generic_array::GenericArray::from_mut_slice(block)); + } + out +} + +/// This proxy's first words to a viewer, as a VNC server: version 3.8 (then "no password"; see +/// Session::feed). +pub const GREETING: &[u8] = b"RFB 003.008\n"; + +/// One person's viewer joined to the box. +pub struct Session { + child: Child, + stdin: ChildStdin, + /// The viewer's side of the handshake still to read: its version (12 bytes) and its choice + /// of security type (1 byte). + greeting: usize, + /// Viewer → box messages not yet complete (to notice clicks and keys). + pending: Vec, + sniffing: bool, + /// The viewer's ClientInit (one byte) is still to come. + init: bool, +} + +/// Open a viewer's way to the box: sign in to its VNC server, and start sending what the server +/// says to `out` (from a thread of its own; `out` returns false when the viewer is gone, and +/// gets an empty slice when the box's side ended). The caller then sends the viewer GREETING +/// (once it is ready for the viewer's answer). +pub fn open(out: impl Fn(&[u8]) -> bool + Send + 'static) -> Result { + crate::cloudbox::ensure_running()?; + let mut child = crate::cloudbox::exec_piped(&["python3", "-c", BRIDGE])?; + let mut stdin = child.stdin.take().ok_or("連不上這台電腦的畫面")?; + let mut stdout = child.stdout.take().ok_or("連不上這台電腦的畫面")?; + let fail = |child: &mut Child, why: &str| -> String { + let _ = child.kill(); + let _ = child.wait(); + format!("連不上這台電腦的畫面:{why}") + }; + let mut read = |n: usize| -> Result, String> { + let mut b = vec![0u8; n]; + stdout.read_exact(&mut b).map(|_| b).map_err(|_| "畫面伺服器沒有回應".to_string()) + }; + // The box's side, as a VNC client: version 3.8, then its password. + let signed_in = (|| -> Result<(), String> { + let v = read(12)?; + if !v.starts_with(b"RFB 003.") { + return Err("不是 VNC 伺服器".into()); + } + stdin.write_all(b"RFB 003.008\n").map_err(|e| e.to_string())?; + let n = read(1)?[0] as usize; + if n == 0 { + return Err("畫面伺服器拒絕連線".into()); + } + let types = read(n)?; + if types.contains(&2) { + stdin.write_all(&[2]).map_err(|e| e.to_string())?; + let c: [u8; 16] = read(16)?.try_into().map_err(|_| "認證失敗".to_string())?; + stdin.write_all(&vnc_response(&crate::cloudbox::box_vnc_password(), &c)).map_err(|e| e.to_string())?; + } else if types.contains(&1) { + stdin.write_all(&[1]).map_err(|e| e.to_string())?; + } else { + return Err("畫面伺服器要的登入方式不支援".into()); + } + if read(4)? != [0, 0, 0, 0] { + return Err("畫面伺服器的密碼不對".into()); + } + Ok(()) + })(); + if let Err(e) = signed_in { + return Err(fail(&mut child, &e)); + } + OPEN.fetch_add(1, Ordering::SeqCst); + crate::cloudbox::set_viewer_open(true); + std::thread::spawn(move || { + let mut buf = vec![0u8; 64 * 1024]; + loop { + match stdout.read(&mut buf) { + Ok(0) | Err(_) => break, + Ok(n) => { + if !out(&buf[..n]) { + return; + } + } + } + } + // The box's side ended: an empty send says so. + out(&[]); + }); + Ok(Session { child, stdin, greeting: 13, pending: Vec::new(), sniffing: true, init: true }) +} + +impl Session { + /// Bytes from the viewer. False when the box's side is gone. + pub fn feed(&mut self, mut b: &[u8], reply: impl Fn(&[u8]) -> bool) -> bool { + if self.greeting > 0 { + let take = self.greeting.min(b.len()); + let before = self.greeting; + self.greeting -= take; + b = &b[take..]; + // After the version (12 bytes): the one security type, "None". + if before > 1 && self.greeting <= 1 && !reply(&[1, 1]) { + return false; + } + // After the choice: OK. + if self.greeting == 0 && !reply(&[0, 0, 0, 0]) { + return false; + } + if b.is_empty() { + return true; + } + } + self.notice_input(b); + self.stdin.write_all(b).and_then(|_| self.stdin.flush()).is_ok() + } + + /// Follow the viewer's messages (RFC 6143 §7.5) to see clicks and keys. Anything it doesn't + /// know stops the watching, never the forwarding. + fn notice_input(&mut self, b: &[u8]) { + if !self.sniffing { + return; + } + self.pending.extend_from_slice(b); + // The first byte after the handshake is ClientInit (the shared flag). + if self.init && !self.pending.is_empty() { + self.pending.remove(0); + self.init = false; + } + let mut at = 0; + loop { + let rest = &self.pending[at..]; + let Some(&kind) = rest.first() else { break }; + let len = match kind { + 0 => 20, + 2 if rest.len() >= 4 => 4 + 4 * u16::from_be_bytes([rest[2], rest[3]]) as usize, + 3 => 10, + 4 => 8, + 5 => 6, + 6 if rest.len() >= 8 => 8 + u32::from_be_bytes([rest[4], rest[5], rest[6], rest[7]]) as usize, + 2 | 6 => break, + _ => { + self.sniffing = false; + self.pending.clear(); + return; + } + }; + if rest.len() < len { + break; + } + match kind { + 4 if rest[1] == 1 => person_acted(), + 5 if rest[1] != 0 => person_acted(), + _ => {} + } + at += len; + } + self.pending.drain(..at); + } +} + +impl Drop for Session { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + if OPEN.fetch_sub(1, Ordering::SeqCst) == 1 { + crate::cloudbox::set_viewer_open(false); + } + } +} + +/// The person clicked or typed: the bot's steps wait until a little after they stop. +pub fn person_acted() { + *LAST_INPUT.lock().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now()); +} + +/// The person is using the box through a viewer right now (input in the last few seconds). +pub fn person_active() -> bool { + LAST_INPUT.lock().unwrap_or_else(|e| e.into_inner()).map(|t| t.elapsed() < HELD_AFTER_INPUT).unwrap_or(false) +} + +/// Text the person typed in the viewer's own box (any language): pasted into the box. +pub fn person_typed(text: &str) { + if !text.is_empty() { + person_acted(); + crate::cloudbox::viewer_text(text); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_bridge_keeps_its_indentation() { + // Python needs its indentation (a Rust "\" line continuation would strip it). + assert!(BRIDGE.contains("\n r=select") && BRIDGE.contains("\n if not d: break")); + } + + #[test] + fn vnc_password_answer_matches_the_standard() { + // Password "password", challenge 00..0f: DES-ECB with the bit-reversed key 0e86ceceeef64e26 + // (worked out with `openssl enc -des-ecb -nopad`). + let c: [u8; 16] = core::array::from_fn(|i| i as u8); + let want = [0xb8, 0x66, 0x92, 0x41, 0x25, 0xc8, 0xee, 0xbb, 0x9d, 0xeb, 0xc1, 0xdb, 0x61, 0xc5, 0x38, 0xe2]; + assert_eq!(vnc_response("password", &c), want); + // Only the first 8 bytes of a password count. + assert_eq!(vnc_response("password-and-more", &c), want); + } +}