From 9f8a00cd04e378f7090f639cc6e99bc666b4eaa8 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 17 May 2026 20:51:07 +0200 Subject: [PATCH 1/7] Fix(Go): Restore missing go.mod from SimpleRemoter migration --- server/go/go.mod | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 server/go/go.mod diff --git a/server/go/go.mod b/server/go/go.mod new file mode 100644 index 0000000..a1d5f04 --- /dev/null +++ b/server/go/go.mod @@ -0,0 +1,16 @@ +module github.com/yuanyuanxiang/SimpleRemoter/server/go + +go 1.24.5 + +require ( + github.com/klauspost/compress v1.18.2 + github.com/rs/zerolog v1.34.0 + golang.org/x/text v0.32.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + golang.org/x/sys v0.12.0 // indirect +) -- 2.43.0 From af2aa4893f520086b59ee6492f0b99911ccd2866 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 17 May 2026 20:56:34 +0200 Subject: [PATCH 2/7] Feature(Go): Embed and serve web UI assets --- .gitignore | 1 + server/go/.vscode/launch.json | 6 +- server/go/.vscode/tasks.json | 20 +++ server/go/Makefile | 21 ++- server/go/README.md | 31 +++- server/go/cmd/main.go | 14 +- server/go/web/assets/static/fit.min.js | 8 + server/go/web/assets/static/xterm.css | 209 +++++++++++++++++++++++ server/go/web/assets/static/xterm.min.js | 8 + server/go/web/embed.go | 23 +++ server/go/web/server.go | 119 +++++++++++++ 11 files changed, 449 insertions(+), 11 deletions(-) create mode 100644 server/go/.vscode/tasks.json create mode 100644 server/go/web/assets/static/fit.min.js create mode 100644 server/go/web/assets/static/xterm.css create mode 100644 server/go/web/assets/static/xterm.min.js create mode 100644 server/go/web/embed.go create mode 100644 server/go/web/server.go diff --git a/.gitignore b/.gitignore index fa79e7e..7ee8739 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ YAMA.code-workspace .vscode/settings.json Bin/* nul +server/go/web/assets/index.html diff --git a/server/go/.vscode/launch.json b/server/go/.vscode/launch.json index 2b2dfed..30d1af3 100644 --- a/server/go/.vscode/launch.json +++ b/server/go/.vscode/launch.json @@ -10,7 +10,8 @@ "cwd": "${workspaceFolder}", "args": [], "env": {}, - "console": "integratedTerminal" + "console": "integratedTerminal", + "preLaunchTask": "sync-web-assets" }, { "name": "Debug Server", @@ -24,7 +25,8 @@ ], "env": {}, "console": "integratedTerminal", - "buildFlags": "-gcflags='all=-N -l'" + "buildFlags": "-gcflags='all=-N -l'", + "preLaunchTask": "sync-web-assets" } ] } \ No newline at end of file diff --git a/server/go/.vscode/tasks.json b/server/go/.vscode/tasks.json new file mode 100644 index 0000000..e06cdad --- /dev/null +++ b/server/go/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "sync-web-assets", + "type": "shell", + "command": "powershell", + "args": [ + "-NoProfile", + "-Command", + "New-Item -ItemType Directory -Force -Path '${workspaceFolder}\\web\\assets' | Out-Null; Copy-Item -Force '${workspaceFolder}\\..\\web\\index.html' '${workspaceFolder}\\web\\assets\\index.html'" + ], + "presentation": { + "reveal": "silent", + "panel": "dedicated" + }, + "problemMatcher": [] + } + ] +} diff --git a/server/go/Makefile b/server/go/Makefile index 565e2ac..f0073ce 100644 --- a/server/go/Makefile +++ b/server/go/Makefile @@ -17,41 +17,49 @@ LDFLAGS=-ldflags "-s -w" MKDIR=if not exist $(BUILD_DIR) mkdir $(BUILD_DIR) RMDIR=if exist $(BUILD_DIR) rmdir /s /q $(BUILD_DIR) -.PHONY: all clean windows linux build help windows-386 linux-arm64 all-platforms test fmt deps +.PHONY: all clean windows linux build help windows-386 linux-arm64 all-platforms test fmt deps sync # Default target all: clean windows linux +# Sync web assets from server/web/ into web/assets/ for //go:embed. +# Single source of truth is server/web/index.html; this just keeps a vendored copy. +sync: + @echo Syncing web assets from ../web/... + @if not exist web\assets mkdir web\assets + @copy /Y ..\web\index.html web\assets\index.html >nul + @echo Done + # Build for current platform -build: +build: sync @echo Building for current platform... @$(MKDIR) go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME).exe $(MAIN_PACKAGE) @echo Done # Build for Windows (amd64) -windows: +windows: sync @echo Building for Windows amd64... @$(MKDIR) set GOOS=windows&& set GOARCH=amd64&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_windows_amd64.exe $(MAIN_PACKAGE) @echo Done: $(BUILD_DIR)\$(BINARY_NAME)_windows_amd64.exe # Build for Windows (386) -windows-386: +windows-386: sync @echo Building for Windows 386... @$(MKDIR) set GOOS=windows&& set GOARCH=386&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_windows_386.exe $(MAIN_PACKAGE) @echo Done: $(BUILD_DIR)\$(BINARY_NAME)_windows_386.exe # Build for Linux (amd64) -linux: +linux: sync @echo Building for Linux amd64... @$(MKDIR) set GOOS=linux&& set GOARCH=amd64&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_linux_amd64 $(MAIN_PACKAGE) @echo Done: $(BUILD_DIR)\$(BINARY_NAME)_linux_amd64 # Build for Linux (arm64) -linux-arm64: +linux-arm64: sync @echo Building for Linux arm64... @$(MKDIR) set GOOS=linux&& set GOARCH=arm64&& go build $(LDFLAGS) -o $(BUILD_DIR)\$(BINARY_NAME)_linux_arm64 $(MAIN_PACKAGE) @@ -89,6 +97,7 @@ help: @echo linux - Build for Linux amd64 @echo linux-arm64 - Build for Linux arm64 @echo all-platforms - Build for all platforms + @echo sync - Sync web assets from ../web/ for //go:embed @echo clean - Clean build directory @echo test - Run tests @echo fmt - Format code diff --git a/server/go/README.md b/server/go/README.md index fb8c6cd..65c50c7 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -25,6 +25,12 @@ server/go/ │ └── pool.go # Goroutine 工作池 ├── logger/ │ └── logger.go # 日志模块 (基于 zerolog) +├── web/ +│ ├── embed.go # //go:embed 嵌入 HTML/xterm.js 等 web 资源 +│ ├── server.go # HTTP server (静态页面 + 后续 WS 信令) +│ └── assets/ +│ ├── index.html # 从 ../../web/index.html sync 而来 (gitignored) +│ └── static/ # 第三方 xterm.js 资源 (checked in) └── cmd/ └── main.go # 程序入口 ``` @@ -42,6 +48,7 @@ server/go/ - **优雅关闭**: 支持信号处理和优雅停机,自动释放资源 - **可配置**: 支持自定义端口、最大连接数、超时时间等 - **日志系统**: 基于 zerolog,支持文件输出、日志轮转、客户端上下线记录 +- **Web UI 服务**: 内建 HTTP server,编译期 `//go:embed` 嵌入页面和静态资源,免外部文件依赖 ## 支持的命令 @@ -67,17 +74,37 @@ go mod tidy ### 编译 +推荐用 Makefile,编译前会自动从 `server/web/` 同步 HTML 到 `web/assets/`: + ```bash -go build -o simpleremoter-server ./cmd +make build # 当前平台 +make windows # Windows amd64 +make linux # Linux amd64 ``` +也可以直接用 `go build`,但要先手动 sync: + +```bash +make sync && go build -o simpleremoter-server ./cmd +``` + +VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。 + ### 运行 ```bash ./simpleremoter-server ``` -服务器默认监听 6543 端口,日志输出到 `logs/server.log`。 +默认监听 TCP 端口 `6543`(被控设备),HTTP 端口 `8080`(浏览器 Web UI)。日志写到 `logs/server.log`。 + +### 命令行参数 + +| 参数 | 默认值 | 说明 | +| ---- | ------ | ---- | +| `-port` / `-p` | `6543` | TCP 监听端口,分号分隔可多端口(如 `6543;6544`) | +| `-http-port` | `8080` | HTTP 监听端口(Web UI),传 `0` 禁用 | +| `-no-console` | `false` | 关闭控制台输出(守护进程模式) | ### 环境变量 diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index 6f17668..d6da965 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -14,6 +14,7 @@ import ( "github.com/yuanyuanxiang/SimpleRemoter/server/go/logger" "github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol" "github.com/yuanyuanxiang/SimpleRemoter/server/go/server" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/web" ) // MyHandler implements the server.Handler interface @@ -232,6 +233,7 @@ func main() { // Parse command line flags portStr := flag.String("port", "6543", "Server listen ports (semicolon-separated, e.g. 6543;6544;6545)") flag.StringVar(portStr, "p", "6543", "Server listen ports (shorthand)") + httpPort := flag.Int("http-port", 8080, "HTTP server port for web UI (0 to disable)") noConsole := flag.Bool("no-console", false, "Disable console output (for daemon mode)") flag.Parse() @@ -288,14 +290,23 @@ func main() { servers = append(servers, srv) } - // Start all servers + // Start all TCP servers for _, srv := range servers { if err := srv.Start(); err != nil { log.Fatal("Failed to start server: %v", err) } } + // Start HTTP server for web UI (Phase 1: serves index.html only) + httpSrv := web.New(*httpPort, log.WithPrefix("Web")) + if err := httpSrv.Start(); err != nil { + log.Fatal("Failed to start HTTP server: %v", err) + } + fmt.Printf("Server started on port(s): %v\n", ports) + if *httpPort != 0 { + fmt.Printf("Web UI on http://localhost:%d/\n", *httpPort) + } fmt.Println("Logs are written to: logs/server.log") fmt.Println("Press Ctrl+C to stop...") @@ -305,6 +316,7 @@ func main() { <-sigChan fmt.Println("\nShutting down...") + httpSrv.Stop() for _, srv := range servers { srv.Stop() } diff --git a/server/go/web/assets/static/fit.min.js b/server/go/web/assets/static/fit.min.js new file mode 100644 index 0000000..7384f10 --- /dev/null +++ b/server/go/web/assets/static/fit.min.js @@ -0,0 +1,8 @@ +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})())); +//# sourceMappingURL=xterm-addon-fit.js.map \ No newline at end of file diff --git a/server/go/web/assets/static/xterm.css b/server/go/web/assets/static/xterm.css new file mode 100644 index 0000000..74acc26 --- /dev/null +++ b/server/go/web/assets/static/xterm.css @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * @license MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +/** + * Default styles for xterm.js + */ + +.xterm { + cursor: text; + position: relative; + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; +} + +.xterm.focus, +.xterm:focus { + outline: none; +} + +.xterm .xterm-helpers { + position: absolute; + top: 0; + /** + * The z-index of the helpers must be higher than the canvases in order for + * IMEs to appear on top. + */ + z-index: 5; +} + +.xterm .xterm-helper-textarea { + padding: 0; + border: 0; + margin: 0; + /* Move textarea out of the screen to the far left, so that the cursor is not visible */ + position: absolute; + opacity: 0; + left: -9999em; + top: 0; + width: 0; + height: 0; + z-index: -5; + /** Prevent wrapping so the IME appears against the textarea at the correct position */ + white-space: nowrap; + overflow: hidden; + resize: none; +} + +.xterm .composition-view { + /* TODO: Composition position got messed up somewhere */ + background: #000; + color: #FFF; + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} + +.xterm .composition-view.active { + display: block; +} + +.xterm .xterm-viewport { + /* On OS X this is required in order for the scroll bar to appear fully opaque */ + background-color: #000; + overflow-y: scroll; + cursor: default; + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; +} + +.xterm .xterm-screen { + position: relative; +} + +.xterm .xterm-screen canvas { + position: absolute; + left: 0; + top: 0; +} + +.xterm .xterm-scroll-area { + visibility: hidden; +} + +.xterm-char-measure-element { + display: inline-block; + visibility: hidden; + position: absolute; + top: 0; + left: -9999em; + line-height: normal; +} + +.xterm.enable-mouse-events { + /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ + cursor: default; +} + +.xterm.xterm-cursor-pointer, +.xterm .xterm-cursor-pointer { + cursor: pointer; +} + +.xterm.column-select.focus { + /* Column selection mode */ + cursor: crosshair; +} + +.xterm .xterm-accessibility, +.xterm .xterm-message { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 10; + color: transparent; + pointer-events: none; +} + +.xterm .live-region { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.xterm-dim { + /* Dim should not apply to background, so the opacity of the foreground color is applied + * explicitly in the generated class and reset to 1 here */ + opacity: 1 !important; +} + +.xterm-underline-1 { text-decoration: underline; } +.xterm-underline-2 { text-decoration: double underline; } +.xterm-underline-3 { text-decoration: wavy underline; } +.xterm-underline-4 { text-decoration: dotted underline; } +.xterm-underline-5 { text-decoration: dashed underline; } + +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + +.xterm-strikethrough { + text-decoration: line-through; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration { + z-index: 6; + position: absolute; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { + z-index: 7; +} + +.xterm-decoration-overview-ruler { + z-index: 8; + position: absolute; + top: 0; + right: 0; + pointer-events: none; +} + +.xterm-decoration-top { + z-index: 2; + position: relative; +} diff --git a/server/go/web/assets/static/xterm.min.js b/server/go/web/assets/static/xterm.min.js new file mode 100644 index 0000000..d7bd63f --- /dev/null +++ b/server/go/web/assets/static/xterm.min.js @@ -0,0 +1,8 @@ +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/xterm@5.3.0/lib/xterm.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(self,(()=>(()=>{"use strict";var e={4567:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.AccessibilityManager=void 0;const n=i(9042),o=i(6114),a=i(9924),h=i(844),c=i(5596),l=i(4725),d=i(3656);let _=t.AccessibilityManager=class extends h.Disposable{constructor(e,t){super(),this._terminal=e,this._renderService=t,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=document.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=document.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let e=0;ethis._handleBoundaryFocus(e,0),this._bottomBoundaryFocusListener=e=>this._handleBoundaryFocus(e,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=document.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new a.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((e=>this._handleResize(e.rows)))),this.register(this._terminal.onRender((e=>this._refreshRows(e.start,e.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((e=>this._handleChar(e)))),this.register(this._terminal.onLineFeed((()=>this._handleChar("\n")))),this.register(this._terminal.onA11yTab((e=>this._handleTab(e)))),this.register(this._terminal.onKey((e=>this._handleKey(e.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this._screenDprMonitor=new c.ScreenDprMonitor(window),this.register(this._screenDprMonitor),this._screenDprMonitor.setListener((()=>this._refreshRowsDimensions())),this.register((0,d.addDisposableDomListener)(window,"resize",(()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,h.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(e){for(let t=0;t0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,"\n"===e&&(this._liveRegionLineCount++,21===this._liveRegionLineCount&&(this._liveRegion.textContent+=n.tooMuchOutput)),o.isMac&&this._liveRegion.textContent&&this._liveRegion.textContent.length>0&&!this._liveRegion.parentNode&&setTimeout((()=>{this._accessibilityContainer.appendChild(this._liveRegion)}),0))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0,o.isMac&&this._liveRegion.remove()}_handleKey(e){this._clearLiveRegion(),/\p{Control}/u.test(e)||this._charsToConsume.push(e)}_refreshRows(e,t){this._liveRegionDebouncer.refresh(e,t,this._terminal.rows)}_renderRows(e,t){const i=this._terminal.buffer,s=i.lines.length.toString();for(let r=e;r<=t;r++){const e=i.translateBufferLineToString(i.ydisp+r,!0),t=(i.ydisp+r+1).toString(),n=this._rowElements[r];n&&(0===e.length?n.innerText=" ":n.textContent=e,n.setAttribute("aria-posinset",t),n.setAttribute("aria-setsize",s))}this._announceCharacters()}_announceCharacters(){0!==this._charsToAnnounce.length&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(e,t){const i=e.target,s=this._rowElements[0===t?1:this._rowElements.length-2];if(i.getAttribute("aria-posinset")===(0===t?"1":`${this._terminal.buffer.lines.length}`))return;if(e.relatedTarget!==s)return;let r,n;if(0===t?(r=i,n=this._rowElements.pop(),this._rowContainer.removeChild(n)):(r=this._rowElements.shift(),n=i,this._rowContainer.removeChild(r)),r.removeEventListener("focus",this._topBoundaryFocusListener),n.removeEventListener("focus",this._bottomBoundaryFocusListener),0===t){const e=this._createAccessibilityTreeNode();this._rowElements.unshift(e),this._rowContainer.insertAdjacentElement("afterbegin",e)}else{const e=this._createAccessibilityTreeNode();this._rowElements.push(e),this._rowContainer.appendChild(e)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(0===t?-1:1),this._rowElements[0===t?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_handleResize(e){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let e=this._rowContainer.children.length;ee;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const e=document.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=`${this._renderService.dimensions.css.canvas.width}px`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let e=0;e{function i(e){return e.replace(/\r?\n/g,"\r")}function s(e,t){return t?"[200~"+e+"[201~":e}function r(e,t,r,n){e=s(e=i(e),r.decPrivateModes.bracketedPasteMode&&!0!==n.rawOptions.ignoreBracketedPasteMode),r.triggerDataEvent(e,!0),t.value=""}function n(e,t,i){const s=i.getBoundingClientRect(),r=e.clientX-s.left-10,n=e.clientY-s.top-10;t.style.width="20px",t.style.height="20px",t.style.left=`${r}px`,t.style.top=`${n}px`,t.style.zIndex="1000",t.focus()}Object.defineProperty(t,"__esModule",{value:!0}),t.rightClickHandler=t.moveTextAreaUnderMouseCursor=t.paste=t.handlePasteEvent=t.copyHandler=t.bracketTextForPaste=t.prepareTextForTerminal=void 0,t.prepareTextForTerminal=i,t.bracketTextForPaste=s,t.copyHandler=function(e,t){e.clipboardData&&e.clipboardData.setData("text/plain",t.selectionText),e.preventDefault()},t.handlePasteEvent=function(e,t,i,s){e.stopPropagation(),e.clipboardData&&r(e.clipboardData.getData("text/plain"),t,i,s)},t.paste=r,t.moveTextAreaUnderMouseCursor=n,t.rightClickHandler=function(e,t,i,s,r){n(e,t,i),r&&s.rightClickSelect(e),t.value=s.selectionText,t.select()}},7239:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ColorContrastCache=void 0;const s=i(1505);t.ColorContrastCache=class{constructor(){this._color=new s.TwoKeyMap,this._css=new s.TwoKeyMap}setCss(e,t,i){this._css.set(e,t,i)}getCss(e,t){return this._css.get(e,t)}setColor(e,t,i){this._color.set(e,t,i)}getColor(e,t){return this._color.get(e,t)}clear(){this._color.clear(),this._css.clear()}}},3656:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.addDisposableDomListener=void 0,t.addDisposableDomListener=function(e,t,i,s){e.addEventListener(t,i,s);let r=!1;return{dispose:()=>{r||(r=!0,e.removeEventListener(t,i,s))}}}},6465:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Linkifier2=void 0;const n=i(3656),o=i(8460),a=i(844),h=i(2585);let c=t.Linkifier2=class extends a.Disposable{get currentLink(){return this._currentLink}constructor(e){super(),this._bufferService=e,this._linkProviders=[],this._linkCacheDisposables=[],this._isMouseOut=!0,this._wasResized=!1,this._activeLine=-1,this._onShowLinkUnderline=this.register(new o.EventEmitter),this.onShowLinkUnderline=this._onShowLinkUnderline.event,this._onHideLinkUnderline=this.register(new o.EventEmitter),this.onHideLinkUnderline=this._onHideLinkUnderline.event,this.register((0,a.getDisposeArrayDisposable)(this._linkCacheDisposables)),this.register((0,a.toDisposable)((()=>{this._lastMouseEvent=void 0}))),this.register(this._bufferService.onResize((()=>{this._clearCurrentLink(),this._wasResized=!0})))}registerLinkProvider(e){return this._linkProviders.push(e),{dispose:()=>{const t=this._linkProviders.indexOf(e);-1!==t&&this._linkProviders.splice(t,1)}}}attachToDom(e,t,i){this._element=e,this._mouseService=t,this._renderService=i,this.register((0,n.addDisposableDomListener)(this._element,"mouseleave",(()=>{this._isMouseOut=!0,this._clearCurrentLink()}))),this.register((0,n.addDisposableDomListener)(this._element,"mousemove",this._handleMouseMove.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,"mousedown",this._handleMouseDown.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,"mouseup",this._handleMouseUp.bind(this)))}_handleMouseMove(e){if(this._lastMouseEvent=e,!this._element||!this._mouseService)return;const t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(!t)return;this._isMouseOut=!1;const i=e.composedPath();for(let e=0;e{null==e||e.forEach((e=>{e.link.dispose&&e.link.dispose()}))})),this._activeProviderReplies=new Map,this._activeLine=e.y);let r=!1;for(const[i,n]of this._linkProviders.entries())t?(null===(s=this._activeProviderReplies)||void 0===s?void 0:s.get(i))&&(r=this._checkLinkProviderResult(i,e,r)):n.provideLinks(e.y,(t=>{var s,n;if(this._isMouseOut)return;const o=null==t?void 0:t.map((e=>({link:e})));null===(s=this._activeProviderReplies)||void 0===s||s.set(i,o),r=this._checkLinkProviderResult(i,e,r),(null===(n=this._activeProviderReplies)||void 0===n?void 0:n.size)===this._linkProviders.length&&this._removeIntersectingLinks(e.y,this._activeProviderReplies)}))}_removeIntersectingLinks(e,t){const i=new Set;for(let s=0;se?this._bufferService.cols:s.link.range.end.x;for(let e=n;e<=o;e++){if(i.has(e)){r.splice(t--,1);break}i.add(e)}}}}_checkLinkProviderResult(e,t,i){var s;if(!this._activeProviderReplies)return i;const r=this._activeProviderReplies.get(e);let n=!1;for(let t=0;tthis._linkAtPosition(e.link,t)));e&&(i=!0,this._handleNewLink(e))}if(this._activeProviderReplies.size===this._linkProviders.length&&!i)for(let e=0;ethis._linkAtPosition(e.link,t)));if(r){i=!0,this._handleNewLink(r);break}}return i}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(e){if(!this._element||!this._mouseService||!this._currentLink)return;const t=this._positionFromMouseEvent(e,this._element,this._mouseService);t&&this._mouseDownLink===this._currentLink&&this._linkAtPosition(this._currentLink.link,t)&&this._currentLink.link.activate(e,this._currentLink.link.text)}_clearCurrentLink(e,t){this._element&&this._currentLink&&this._lastMouseEvent&&(!e||!t||this._currentLink.link.range.start.y>=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,(0,a.disposeArray)(this._linkCacheDisposables))}_handleNewLink(e){if(!this._element||!this._lastMouseEvent||!this._mouseService)return;const t=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);t&&this._linkAtPosition(e.link,t)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:void 0===e.link.decorations||e.link.decorations.underline,pointerCursor:void 0===e.link.decorations||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:()=>{var e,t;return null===(t=null===(e=this._currentLink)||void 0===e?void 0:e.state)||void 0===t?void 0:t.decorations.pointerCursor},set:e=>{var t,i;(null===(t=this._currentLink)||void 0===t?void 0:t.state)&&this._currentLink.state.decorations.pointerCursor!==e&&(this._currentLink.state.decorations.pointerCursor=e,this._currentLink.state.isHovered&&(null===(i=this._element)||void 0===i||i.classList.toggle("xterm-cursor-pointer",e)))}},underline:{get:()=>{var e,t;return null===(t=null===(e=this._currentLink)||void 0===e?void 0:e.state)||void 0===t?void 0:t.decorations.underline},set:t=>{var i,s,r;(null===(i=this._currentLink)||void 0===i?void 0:i.state)&&(null===(r=null===(s=this._currentLink)||void 0===s?void 0:s.state)||void 0===r?void 0:r.decorations.underline)!==t&&(this._currentLink.state.decorations.underline=t,this._currentLink.state.isHovered&&this._fireUnderlineEvent(e.link,t))}}}),this._renderService&&this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange((e=>{if(!this._currentLink)return;const t=0===e.start?0:e.start+1+this._bufferService.buffer.ydisp,i=this._bufferService.buffer.ydisp+1+e.end;if(this._currentLink.link.range.start.y>=t&&this._currentLink.link.range.end.y<=i&&(this._clearCurrentLink(t,i),this._lastMouseEvent&&this._element)){const e=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);e&&this._askForLink(e,!1)}}))))}_linkHover(e,t,i){var s;(null===(s=this._currentLink)||void 0===s?void 0:s.state)&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add("xterm-cursor-pointer")),t.hover&&t.hover(i,t.text)}_fireUnderlineEvent(e,t){const i=e.range,s=this._bufferService.buffer.ydisp,r=this._createLinkUnderlineEvent(i.start.x-1,i.start.y-s-1,i.end.x,i.end.y-s-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(r)}_linkLeave(e,t,i){var s;(null===(s=this._currentLink)||void 0===s?void 0:s.state)&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove("xterm-cursor-pointer")),t.leave&&t.leave(i,t.text)}_linkAtPosition(e,t){const i=e.range.start.y*this._bufferService.cols+e.range.start.x,s=e.range.end.y*this._bufferService.cols+e.range.end.x,r=t.y*this._bufferService.cols+t.x;return i<=r&&r<=s}_positionFromMouseEvent(e,t,i){const s=i.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(s)return{x:s[0],y:s[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(e,t,i,s,r){return{x1:e,y1:t,x2:i,y2:s,cols:this._bufferService.cols,fg:r}}};t.Linkifier2=c=s([r(0,h.IBufferService)],c)},9042:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.tooMuchOutput=t.promptLabel=void 0,t.promptLabel="Terminal input",t.tooMuchOutput="Too much output to announce, navigate to rows manually to read"},3730:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.OscLinkProvider=void 0;const n=i(511),o=i(2585);let a=t.OscLinkProvider=class{constructor(e,t,i){this._bufferService=e,this._optionsService=t,this._oscLinkService=i}provideLinks(e,t){var i;const s=this._bufferService.buffer.lines.get(e-1);if(!s)return void t(void 0);const r=[],o=this._optionsService.rawOptions.linkHandler,a=new n.CellData,c=s.getTrimmedLength();let l=-1,d=-1,_=!1;for(let t=0;to?o.activate(e,t,i):h(0,t),hover:(e,t)=>{var s;return null===(s=null==o?void 0:o.hover)||void 0===s?void 0:s.call(o,e,t,i)},leave:(e,t)=>{var s;return null===(s=null==o?void 0:o.leave)||void 0===s?void 0:s.call(o,e,t,i)}})}_=!1,a.hasExtendedAttrs()&&a.extended.urlId?(d=t,l=a.extended.urlId):(d=-1,l=-1)}}t(r)}};function h(e,t){if(confirm(`Do you want to navigate to ${t}?\n\nWARNING: This link could potentially be dangerous`)){const e=window.open();if(e){try{e.opener=null}catch(e){}e.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}}t.OscLinkProvider=a=s([r(0,o.IBufferService),r(1,o.IOptionsService),r(2,o.IOscLinkService)],a)},6193:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.RenderDebouncer=void 0,t.RenderDebouncer=class{constructor(e,t){this._parentWindow=e,this._renderCallback=t,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._parentWindow.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(e){return this._refreshCallbacks.push(e),this._animationFrame||(this._animationFrame=this._parentWindow.requestAnimationFrame((()=>this._innerRefresh()))),this._animationFrame}refresh(e,t,i){this._rowCount=i,e=void 0!==e?e:0,t=void 0!==t?t:this._rowCount-1,this._rowStart=void 0!==this._rowStart?Math.min(this._rowStart,e):e,this._rowEnd=void 0!==this._rowEnd?Math.max(this._rowEnd,t):t,this._animationFrame||(this._animationFrame=this._parentWindow.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._animationFrame=void 0,void 0===this._rowStart||void 0===this._rowEnd||void 0===this._rowCount)return void this._runRefreshCallbacks();const e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(const e of this._refreshCallbacks)e(0);this._refreshCallbacks=[]}}},5596:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ScreenDprMonitor=void 0;const s=i(844);class r extends s.Disposable{constructor(e){super(),this._parentWindow=e,this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this.register((0,s.toDisposable)((()=>{this.clearListener()})))}setListener(e){this._listener&&this.clearListener(),this._listener=e,this._outerListener=()=>{this._listener&&(this._listener(this._parentWindow.devicePixelRatio,this._currentDevicePixelRatio),this._updateDpr())},this._updateDpr()}_updateDpr(){var e;this._outerListener&&(null===(e=this._resolutionMediaMatchList)||void 0===e||e.removeListener(this._outerListener),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._resolutionMediaMatchList=this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`),this._resolutionMediaMatchList.addListener(this._outerListener))}clearListener(){this._resolutionMediaMatchList&&this._listener&&this._outerListener&&(this._resolutionMediaMatchList.removeListener(this._outerListener),this._resolutionMediaMatchList=void 0,this._listener=void 0,this._outerListener=void 0)}}t.ScreenDprMonitor=r},3236:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Terminal=void 0;const s=i(3614),r=i(3656),n=i(6465),o=i(9042),a=i(3730),h=i(1680),c=i(3107),l=i(5744),d=i(2950),_=i(1296),u=i(428),f=i(4269),v=i(5114),p=i(8934),g=i(3230),m=i(9312),S=i(4725),C=i(6731),b=i(8055),y=i(8969),w=i(8460),E=i(844),k=i(6114),L=i(8437),D=i(2584),R=i(7399),x=i(5941),A=i(9074),B=i(2585),T=i(5435),M=i(4567),O="undefined"!=typeof window?window.document:null;class P extends y.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(e={}){super(e),this.browser=k,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new E.MutableDisposable),this._onCursorMove=this.register(new w.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new w.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new w.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new w.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new w.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new w.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new w.EventEmitter),this._onBlur=this.register(new w.EventEmitter),this._onA11yCharEmitter=this.register(new w.EventEmitter),this._onA11yTabEmitter=this.register(new w.EventEmitter),this._onWillOpen=this.register(new w.EventEmitter),this._setup(),this.linkifier2=this.register(this._instantiationService.createInstance(n.Linkifier2)),this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(a.OscLinkProvider)),this._decorationService=this._instantiationService.createInstance(A.DecorationService),this._instantiationService.setService(B.IDecorationService,this._decorationService),this.register(this._inputHandler.onRequestBell((()=>this._onBell.fire()))),this.register(this._inputHandler.onRequestRefreshRows(((e,t)=>this.refresh(e,t)))),this.register(this._inputHandler.onRequestSendFocus((()=>this._reportFocus()))),this.register(this._inputHandler.onRequestReset((()=>this.reset()))),this.register(this._inputHandler.onRequestWindowsOptionsReport((e=>this._reportWindowsOptions(e)))),this.register(this._inputHandler.onColor((e=>this._handleColorEvent(e)))),this.register((0,w.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,w.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,w.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,w.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize((e=>this._afterResize(e.cols,e.rows)))),this.register((0,E.toDisposable)((()=>{var e,t;this._customKeyEventHandler=void 0,null===(t=null===(e=this.element)||void 0===e?void 0:e.parentNode)||void 0===t||t.removeChild(this.element)})))}_handleColorEvent(e){if(this._themeService)for(const t of e){let e,i="";switch(t.index){case 256:e="foreground",i="10";break;case 257:e="background",i="11";break;case 258:e="cursor",i="12";break;default:e="ansi",i="4;"+t.index}switch(t.type){case 0:const s=b.color.toColorRGB("ansi"===e?this._themeService.colors.ansi[t.index]:this._themeService.colors[e]);this.coreService.triggerDataEvent(`${D.C0.ESC}]${i};${(0,x.toRgbString)(s)}${D.C1_ESCAPED.ST}`);break;case 1:if("ansi"===e)this._themeService.modifyColors((e=>e.ansi[t.index]=b.rgba.toColor(...t.color)));else{const i=e;this._themeService.modifyColors((e=>e[i]=b.rgba.toColor(...t.color)))}break;case 2:this._themeService.restoreColor(t.index)}}}_setup(){super._setup(),this._customKeyEventHandler=void 0}get buffer(){return this.buffers.active}focus(){this.textarea&&this.textarea.focus({preventScroll:!0})}_handleScreenReaderModeOptionChange(e){e?!this._accessibilityManager.value&&this._renderService&&(this._accessibilityManager.value=this._instantiationService.createInstance(M.AccessibilityManager,this)):this._accessibilityManager.clear()}_handleTextAreaFocus(e){this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(D.C0.ESC+"[I"),this.updateCursorStyle(e),this.element.classList.add("focus"),this._showCursor(),this._onFocus.fire()}blur(){var e;return null===(e=this.textarea)||void 0===e?void 0:e.blur()}_handleTextAreaBlur(){this.textarea.value="",this.refresh(this.buffer.y,this.buffer.y),this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(D.C0.ESC+"[O"),this.element.classList.remove("focus"),this._onBlur.fire()}_syncTextArea(){if(!this.textarea||!this.buffer.isCursorInViewport||this._compositionHelper.isComposing||!this._renderService)return;const e=this.buffer.ybase+this.buffer.y,t=this.buffer.lines.get(e);if(!t)return;const i=Math.min(this.buffer.x,this.cols-1),s=this._renderService.dimensions.css.cell.height,r=t.getWidth(i),n=this._renderService.dimensions.css.cell.width*r,o=this.buffer.y*this._renderService.dimensions.css.cell.height,a=i*this._renderService.dimensions.css.cell.width;this.textarea.style.left=a+"px",this.textarea.style.top=o+"px",this.textarea.style.width=n+"px",this.textarea.style.height=s+"px",this.textarea.style.lineHeight=s+"px",this.textarea.style.zIndex="-5"}_initGlobal(){this._bindKeys(),this.register((0,r.addDisposableDomListener)(this.element,"copy",(e=>{this.hasSelection()&&(0,s.copyHandler)(e,this._selectionService)})));const e=e=>(0,s.handlePasteEvent)(e,this.textarea,this.coreService,this.optionsService);this.register((0,r.addDisposableDomListener)(this.textarea,"paste",e)),this.register((0,r.addDisposableDomListener)(this.element,"paste",e)),k.isFirefox?this.register((0,r.addDisposableDomListener)(this.element,"mousedown",(e=>{2===e.button&&(0,s.rightClickHandler)(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)}))):this.register((0,r.addDisposableDomListener)(this.element,"contextmenu",(e=>{(0,s.rightClickHandler)(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)}))),k.isLinux&&this.register((0,r.addDisposableDomListener)(this.element,"auxclick",(e=>{1===e.button&&(0,s.moveTextAreaUnderMouseCursor)(e,this.textarea,this.screenElement)})))}_bindKeys(){this.register((0,r.addDisposableDomListener)(this.textarea,"keyup",(e=>this._keyUp(e)),!0)),this.register((0,r.addDisposableDomListener)(this.textarea,"keydown",(e=>this._keyDown(e)),!0)),this.register((0,r.addDisposableDomListener)(this.textarea,"keypress",(e=>this._keyPress(e)),!0)),this.register((0,r.addDisposableDomListener)(this.textarea,"compositionstart",(()=>this._compositionHelper.compositionstart()))),this.register((0,r.addDisposableDomListener)(this.textarea,"compositionupdate",(e=>this._compositionHelper.compositionupdate(e)))),this.register((0,r.addDisposableDomListener)(this.textarea,"compositionend",(()=>this._compositionHelper.compositionend()))),this.register((0,r.addDisposableDomListener)(this.textarea,"input",(e=>this._inputEvent(e)),!0)),this.register(this.onRender((()=>this._compositionHelper.updateCompositionElements())))}open(e){var t;if(!e)throw new Error("Terminal requires a parent element.");e.isConnected||this._logService.debug("Terminal.open was called on an element that was not attached to the DOM"),this._document=e.ownerDocument,this.element=this._document.createElement("div"),this.element.dir="ltr",this.element.classList.add("terminal"),this.element.classList.add("xterm"),e.appendChild(this.element);const i=O.createDocumentFragment();this._viewportElement=O.createElement("div"),this._viewportElement.classList.add("xterm-viewport"),i.appendChild(this._viewportElement),this._viewportScrollArea=O.createElement("div"),this._viewportScrollArea.classList.add("xterm-scroll-area"),this._viewportElement.appendChild(this._viewportScrollArea),this.screenElement=O.createElement("div"),this.screenElement.classList.add("xterm-screen"),this._helperContainer=O.createElement("div"),this._helperContainer.classList.add("xterm-helpers"),this.screenElement.appendChild(this._helperContainer),i.appendChild(this.screenElement),this.textarea=O.createElement("textarea"),this.textarea.classList.add("xterm-helper-textarea"),this.textarea.setAttribute("aria-label",o.promptLabel),k.isChromeOS||this.textarea.setAttribute("aria-multiline","false"),this.textarea.setAttribute("autocorrect","off"),this.textarea.setAttribute("autocapitalize","off"),this.textarea.setAttribute("spellcheck","false"),this.textarea.tabIndex=0,this._coreBrowserService=this._instantiationService.createInstance(v.CoreBrowserService,this.textarea,null!==(t=this._document.defaultView)&&void 0!==t?t:window),this._instantiationService.setService(S.ICoreBrowserService,this._coreBrowserService),this.register((0,r.addDisposableDomListener)(this.textarea,"focus",(e=>this._handleTextAreaFocus(e)))),this.register((0,r.addDisposableDomListener)(this.textarea,"blur",(()=>this._handleTextAreaBlur()))),this._helperContainer.appendChild(this.textarea),this._charSizeService=this._instantiationService.createInstance(u.CharSizeService,this._document,this._helperContainer),this._instantiationService.setService(S.ICharSizeService,this._charSizeService),this._themeService=this._instantiationService.createInstance(C.ThemeService),this._instantiationService.setService(S.IThemeService,this._themeService),this._characterJoinerService=this._instantiationService.createInstance(f.CharacterJoinerService),this._instantiationService.setService(S.ICharacterJoinerService,this._characterJoinerService),this._renderService=this.register(this._instantiationService.createInstance(g.RenderService,this.rows,this.screenElement)),this._instantiationService.setService(S.IRenderService,this._renderService),this.register(this._renderService.onRenderedViewportChange((e=>this._onRender.fire(e)))),this.onResize((e=>this._renderService.resize(e.cols,e.rows))),this._compositionView=O.createElement("div"),this._compositionView.classList.add("composition-view"),this._compositionHelper=this._instantiationService.createInstance(d.CompositionHelper,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this.element.appendChild(i);try{this._onWillOpen.fire(this.element)}catch(e){}this._renderService.hasRenderer()||this._renderService.setRenderer(this._createRenderer()),this._mouseService=this._instantiationService.createInstance(p.MouseService),this._instantiationService.setService(S.IMouseService,this._mouseService),this.viewport=this._instantiationService.createInstance(h.Viewport,this._viewportElement,this._viewportScrollArea),this.viewport.onRequestScrollLines((e=>this.scrollLines(e.amount,e.suppressScrollEvent,1))),this.register(this._inputHandler.onRequestSyncScrollBar((()=>this.viewport.syncScrollArea()))),this.register(this.viewport),this.register(this.onCursorMove((()=>{this._renderService.handleCursorMove(),this._syncTextArea()}))),this.register(this.onResize((()=>this._renderService.handleResize(this.cols,this.rows)))),this.register(this.onBlur((()=>this._renderService.handleBlur()))),this.register(this.onFocus((()=>this._renderService.handleFocus()))),this.register(this._renderService.onDimensionsChange((()=>this.viewport.syncScrollArea()))),this._selectionService=this.register(this._instantiationService.createInstance(m.SelectionService,this.element,this.screenElement,this.linkifier2)),this._instantiationService.setService(S.ISelectionService,this._selectionService),this.register(this._selectionService.onRequestScrollLines((e=>this.scrollLines(e.amount,e.suppressScrollEvent)))),this.register(this._selectionService.onSelectionChange((()=>this._onSelectionChange.fire()))),this.register(this._selectionService.onRequestRedraw((e=>this._renderService.handleSelectionChanged(e.start,e.end,e.columnSelectMode)))),this.register(this._selectionService.onLinuxMouseSelection((e=>{this.textarea.value=e,this.textarea.focus(),this.textarea.select()}))),this.register(this._onScroll.event((e=>{this.viewport.syncScrollArea(),this._selectionService.refresh()}))),this.register((0,r.addDisposableDomListener)(this._viewportElement,"scroll",(()=>this._selectionService.refresh()))),this.linkifier2.attachToDom(this.screenElement,this._mouseService,this._renderService),this.register(this._instantiationService.createInstance(c.BufferDecorationRenderer,this.screenElement)),this.register((0,r.addDisposableDomListener)(this.element,"mousedown",(e=>this._selectionService.handleMouseDown(e)))),this.coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add("enable-mouse-events")):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager.value=this._instantiationService.createInstance(M.AccessibilityManager,this)),this.register(this.optionsService.onSpecificOptionChange("screenReaderMode",(e=>this._handleScreenReaderModeOptionChange(e)))),this.options.overviewRulerWidth&&(this._overviewRulerRenderer=this.register(this._instantiationService.createInstance(l.OverviewRulerRenderer,this._viewportElement,this.screenElement))),this.optionsService.onSpecificOptionChange("overviewRulerWidth",(e=>{!this._overviewRulerRenderer&&e&&this._viewportElement&&this.screenElement&&(this._overviewRulerRenderer=this.register(this._instantiationService.createInstance(l.OverviewRulerRenderer,this._viewportElement,this.screenElement)))})),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()}_createRenderer(){return this._instantiationService.createInstance(_.DomRenderer,this.element,this.screenElement,this._viewportElement,this.linkifier2)}bindMouse(){const e=this,t=this.element;function i(t){const i=e._mouseService.getMouseReportCoords(t,e.screenElement);if(!i)return!1;let s,r;switch(t.overrideType||t.type){case"mousemove":r=32,void 0===t.buttons?(s=3,void 0!==t.button&&(s=t.button<3?t.button:3)):s=1&t.buttons?0:4&t.buttons?1:2&t.buttons?2:3;break;case"mouseup":r=0,s=t.button<3?t.button:3;break;case"mousedown":r=1,s=t.button<3?t.button:3;break;case"wheel":if(0===e.viewport.getLinesScrolled(t))return!1;r=t.deltaY<0?0:1,s=4;break;default:return!1}return!(void 0===r||void 0===s||s>4)&&e.coreMouseService.triggerMouseEvent({col:i.col,row:i.row,x:i.x,y:i.y,button:s,action:r,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey})}const s={mouseup:null,wheel:null,mousedrag:null,mousemove:null},n={mouseup:e=>(i(e),e.buttons||(this._document.removeEventListener("mouseup",s.mouseup),s.mousedrag&&this._document.removeEventListener("mousemove",s.mousedrag)),this.cancel(e)),wheel:e=>(i(e),this.cancel(e,!0)),mousedrag:e=>{e.buttons&&i(e)},mousemove:e=>{e.buttons||i(e)}};this.register(this.coreMouseService.onProtocolChange((e=>{e?("debug"===this.optionsService.rawOptions.logLevel&&this._logService.debug("Binding to mouse events:",this.coreMouseService.explainEvents(e)),this.element.classList.add("enable-mouse-events"),this._selectionService.disable()):(this._logService.debug("Unbinding from mouse events."),this.element.classList.remove("enable-mouse-events"),this._selectionService.enable()),8&e?s.mousemove||(t.addEventListener("mousemove",n.mousemove),s.mousemove=n.mousemove):(t.removeEventListener("mousemove",s.mousemove),s.mousemove=null),16&e?s.wheel||(t.addEventListener("wheel",n.wheel,{passive:!1}),s.wheel=n.wheel):(t.removeEventListener("wheel",s.wheel),s.wheel=null),2&e?s.mouseup||(t.addEventListener("mouseup",n.mouseup),s.mouseup=n.mouseup):(this._document.removeEventListener("mouseup",s.mouseup),t.removeEventListener("mouseup",s.mouseup),s.mouseup=null),4&e?s.mousedrag||(s.mousedrag=n.mousedrag):(this._document.removeEventListener("mousemove",s.mousedrag),s.mousedrag=null)}))),this.coreMouseService.activeProtocol=this.coreMouseService.activeProtocol,this.register((0,r.addDisposableDomListener)(t,"mousedown",(e=>{if(e.preventDefault(),this.focus(),this.coreMouseService.areMouseEventsActive&&!this._selectionService.shouldForceSelection(e))return i(e),s.mouseup&&this._document.addEventListener("mouseup",s.mouseup),s.mousedrag&&this._document.addEventListener("mousemove",s.mousedrag),this.cancel(e)}))),this.register((0,r.addDisposableDomListener)(t,"wheel",(e=>{if(!s.wheel){if(!this.buffer.hasScrollback){const t=this.viewport.getLinesScrolled(e);if(0===t)return;const i=D.C0.ESC+(this.coreService.decPrivateModes.applicationCursorKeys?"O":"[")+(e.deltaY<0?"A":"B");let s="";for(let e=0;e{if(!this.coreMouseService.areMouseEventsActive)return this.viewport.handleTouchStart(e),this.cancel(e)}),{passive:!0})),this.register((0,r.addDisposableDomListener)(t,"touchmove",(e=>{if(!this.coreMouseService.areMouseEventsActive)return this.viewport.handleTouchMove(e)?void 0:this.cancel(e)}),{passive:!1}))}refresh(e,t){var i;null===(i=this._renderService)||void 0===i||i.refreshRows(e,t)}updateCursorStyle(e){var t;(null===(t=this._selectionService)||void 0===t?void 0:t.shouldColumnSelect(e))?this.element.classList.add("column-select"):this.element.classList.remove("column-select")}_showCursor(){this.coreService.isCursorInitialized||(this.coreService.isCursorInitialized=!0,this.refresh(this.buffer.y,this.buffer.y))}scrollLines(e,t,i=0){var s;1===i?(super.scrollLines(e,t,i),this.refresh(0,this.rows-1)):null===(s=this.viewport)||void 0===s||s.scrollLines(e)}paste(e){(0,s.paste)(e,this.textarea,this.coreService,this.optionsService)}attachCustomKeyEventHandler(e){this._customKeyEventHandler=e}registerLinkProvider(e){return this.linkifier2.registerLinkProvider(e)}registerCharacterJoiner(e){if(!this._characterJoinerService)throw new Error("Terminal must be opened first");const t=this._characterJoinerService.register(e);return this.refresh(0,this.rows-1),t}deregisterCharacterJoiner(e){if(!this._characterJoinerService)throw new Error("Terminal must be opened first");this._characterJoinerService.deregister(e)&&this.refresh(0,this.rows-1)}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}registerDecoration(e){return this._decorationService.registerDecoration(e)}hasSelection(){return!!this._selectionService&&this._selectionService.hasSelection}select(e,t,i){this._selectionService.setSelection(e,t,i)}getSelection(){return this._selectionService?this._selectionService.selectionText:""}getSelectionPosition(){if(this._selectionService&&this._selectionService.hasSelection)return{start:{x:this._selectionService.selectionStart[0],y:this._selectionService.selectionStart[1]},end:{x:this._selectionService.selectionEnd[0],y:this._selectionService.selectionEnd[1]}}}clearSelection(){var e;null===(e=this._selectionService)||void 0===e||e.clearSelection()}selectAll(){var e;null===(e=this._selectionService)||void 0===e||e.selectAll()}selectLines(e,t){var i;null===(i=this._selectionService)||void 0===i||i.selectLines(e,t)}_keyDown(e){if(this._keyDownHandled=!1,this._keyDownSeen=!0,this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;const t=this.browser.isMac&&this.options.macOptionIsMeta&&e.altKey;if(!t&&!this._compositionHelper.keydown(e))return this.options.scrollOnUserInput&&this.buffer.ybase!==this.buffer.ydisp&&this.scrollToBottom(),!1;t||"Dead"!==e.key&&"AltGraph"!==e.key||(this._unprocessedDeadKey=!0);const i=(0,R.evaluateKeyboardEvent)(e,this.coreService.decPrivateModes.applicationCursorKeys,this.browser.isMac,this.options.macOptionIsMeta);if(this.updateCursorStyle(e),3===i.type||2===i.type){const t=this.rows-1;return this.scrollLines(2===i.type?-t:t),this.cancel(e,!0)}return 1===i.type&&this.selectAll(),!!this._isThirdLevelShift(this.browser,e)||(i.cancel&&this.cancel(e,!0),!i.key||!!(e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&1===e.key.length&&e.key.charCodeAt(0)>=65&&e.key.charCodeAt(0)<=90)||(this._unprocessedDeadKey?(this._unprocessedDeadKey=!1,!0):(i.key!==D.C0.ETX&&i.key!==D.C0.CR||(this.textarea.value=""),this._onKey.fire({key:i.key,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(i.key,!0),!this.optionsService.rawOptions.screenReaderMode||e.altKey||e.ctrlKey?this.cancel(e,!0):void(this._keyDownHandled=!0))))}_isThirdLevelShift(e,t){const i=e.isMac&&!this.options.macOptionIsMeta&&t.altKey&&!t.ctrlKey&&!t.metaKey||e.isWindows&&t.altKey&&t.ctrlKey&&!t.metaKey||e.isWindows&&t.getModifierState("AltGraph");return"keypress"===t.type?i:i&&(!t.keyCode||t.keyCode>47)}_keyUp(e){this._keyDownSeen=!1,this._customKeyEventHandler&&!1===this._customKeyEventHandler(e)||(function(e){return 16===e.keyCode||17===e.keyCode||18===e.keyCode}(e)||this.focus(),this.updateCursorStyle(e),this._keyPressHandled=!1)}_keyPress(e){let t;if(this._keyPressHandled=!1,this._keyDownHandled)return!1;if(this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(null===e.which||void 0===e.which)t=e.keyCode;else{if(0===e.which||0===e.charCode)return!1;t=e.which}return!(!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e)||(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(t,!0),this._keyPressHandled=!0,this._unprocessedDeadKey=!1,0))}_inputEvent(e){if(e.data&&"insertText"===e.inputType&&(!e.composed||!this._keyDownSeen)&&!this.optionsService.rawOptions.screenReaderMode){if(this._keyPressHandled)return!1;this._unprocessedDeadKey=!1;const t=e.data;return this.coreService.triggerDataEvent(t,!0),this.cancel(e),!0}return!1}resize(e,t){e!==this.cols||t!==this.rows?super.resize(e,t):this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure()}_afterResize(e,t){var i,s;null===(i=this._charSizeService)||void 0===i||i.measure(),null===(s=this.viewport)||void 0===s||s.syncScrollArea(!0)}clear(){var e;if(0!==this.buffer.ybase||0!==this.buffer.y){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e{Object.defineProperty(t,"__esModule",{value:!0}),t.TimeBasedDebouncer=void 0,t.TimeBasedDebouncer=class{constructor(e,t=1e3){this._renderCallback=e,this._debounceThresholdMS=t,this._lastRefreshMs=0,this._additionalRefreshRequested=!1}dispose(){this._refreshTimeoutID&&clearTimeout(this._refreshTimeoutID)}refresh(e,t,i){this._rowCount=i,e=void 0!==e?e:0,t=void 0!==t?t:this._rowCount-1,this._rowStart=void 0!==this._rowStart?Math.min(this._rowStart,e):e,this._rowEnd=void 0!==this._rowEnd?Math.max(this._rowEnd,t):t;const s=Date.now();if(s-this._lastRefreshMs>=this._debounceThresholdMS)this._lastRefreshMs=s,this._innerRefresh();else if(!this._additionalRefreshRequested){const e=s-this._lastRefreshMs,t=this._debounceThresholdMS-e;this._additionalRefreshRequested=!0,this._refreshTimeoutID=window.setTimeout((()=>{this._lastRefreshMs=Date.now(),this._innerRefresh(),this._additionalRefreshRequested=!1,this._refreshTimeoutID=void 0}),t)}}_innerRefresh(){if(void 0===this._rowStart||void 0===this._rowEnd||void 0===this._rowCount)return;const e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t)}}},1680:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Viewport=void 0;const n=i(3656),o=i(4725),a=i(8460),h=i(844),c=i(2585);let l=t.Viewport=class extends h.Disposable{constructor(e,t,i,s,r,o,h,c){super(),this._viewportElement=e,this._scrollArea=t,this._bufferService=i,this._optionsService=s,this._charSizeService=r,this._renderService=o,this._coreBrowserService=h,this.scrollBarWidth=0,this._currentRowHeight=0,this._currentDeviceCellHeight=0,this._lastRecordedBufferLength=0,this._lastRecordedViewportHeight=0,this._lastRecordedBufferHeight=0,this._lastTouchY=0,this._lastScrollTop=0,this._wheelPartialScroll=0,this._refreshAnimationFrame=null,this._ignoreNextScrollEvent=!1,this._smoothScrollState={startTime:0,origin:-1,target:-1},this._onRequestScrollLines=this.register(new a.EventEmitter),this.onRequestScrollLines=this._onRequestScrollLines.event,this.scrollBarWidth=this._viewportElement.offsetWidth-this._scrollArea.offsetWidth||15,this.register((0,n.addDisposableDomListener)(this._viewportElement,"scroll",this._handleScroll.bind(this))),this._activeBuffer=this._bufferService.buffer,this.register(this._bufferService.buffers.onBufferActivate((e=>this._activeBuffer=e.activeBuffer))),this._renderDimensions=this._renderService.dimensions,this.register(this._renderService.onDimensionsChange((e=>this._renderDimensions=e))),this._handleThemeChange(c.colors),this.register(c.onChangeColors((e=>this._handleThemeChange(e)))),this.register(this._optionsService.onSpecificOptionChange("scrollback",(()=>this.syncScrollArea()))),setTimeout((()=>this.syncScrollArea()))}_handleThemeChange(e){this._viewportElement.style.backgroundColor=e.background.css}reset(){this._currentRowHeight=0,this._currentDeviceCellHeight=0,this._lastRecordedBufferLength=0,this._lastRecordedViewportHeight=0,this._lastRecordedBufferHeight=0,this._lastTouchY=0,this._lastScrollTop=0,this._coreBrowserService.window.requestAnimationFrame((()=>this.syncScrollArea()))}_refresh(e){if(e)return this._innerRefresh(),void(null!==this._refreshAnimationFrame&&this._coreBrowserService.window.cancelAnimationFrame(this._refreshAnimationFrame));null===this._refreshAnimationFrame&&(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._charSizeService.height>0){this._currentRowHeight=this._renderService.dimensions.device.cell.height/this._coreBrowserService.dpr,this._currentDeviceCellHeight=this._renderService.dimensions.device.cell.height,this._lastRecordedViewportHeight=this._viewportElement.offsetHeight;const e=Math.round(this._currentRowHeight*this._lastRecordedBufferLength)+(this._lastRecordedViewportHeight-this._renderService.dimensions.css.canvas.height);this._lastRecordedBufferHeight!==e&&(this._lastRecordedBufferHeight=e,this._scrollArea.style.height=this._lastRecordedBufferHeight+"px")}const e=this._bufferService.buffer.ydisp*this._currentRowHeight;this._viewportElement.scrollTop!==e&&(this._ignoreNextScrollEvent=!0,this._viewportElement.scrollTop=e),this._refreshAnimationFrame=null}syncScrollArea(e=!1){if(this._lastRecordedBufferLength!==this._bufferService.buffer.lines.length)return this._lastRecordedBufferLength=this._bufferService.buffer.lines.length,void this._refresh(e);this._lastRecordedViewportHeight===this._renderService.dimensions.css.canvas.height&&this._lastScrollTop===this._activeBuffer.ydisp*this._currentRowHeight&&this._renderDimensions.device.cell.height===this._currentDeviceCellHeight||this._refresh(e)}_handleScroll(e){if(this._lastScrollTop=this._viewportElement.scrollTop,!this._viewportElement.offsetParent)return;if(this._ignoreNextScrollEvent)return this._ignoreNextScrollEvent=!1,void this._onRequestScrollLines.fire({amount:0,suppressScrollEvent:!0});const t=Math.round(this._lastScrollTop/this._currentRowHeight)-this._bufferService.buffer.ydisp;this._onRequestScrollLines.fire({amount:t,suppressScrollEvent:!0})}_smoothScroll(){if(this._isDisposed||-1===this._smoothScrollState.origin||-1===this._smoothScrollState.target)return;const e=this._smoothScrollPercent();this._viewportElement.scrollTop=this._smoothScrollState.origin+Math.round(e*(this._smoothScrollState.target-this._smoothScrollState.origin)),e<1?this._coreBrowserService.window.requestAnimationFrame((()=>this._smoothScroll())):this._clearSmoothScrollState()}_smoothScrollPercent(){return this._optionsService.rawOptions.smoothScrollDuration&&this._smoothScrollState.startTime?Math.max(Math.min((Date.now()-this._smoothScrollState.startTime)/this._optionsService.rawOptions.smoothScrollDuration,1),0):1}_clearSmoothScrollState(){this._smoothScrollState.startTime=0,this._smoothScrollState.origin=-1,this._smoothScrollState.target=-1}_bubbleScroll(e,t){const i=this._viewportElement.scrollTop+this._lastRecordedViewportHeight;return!(t<0&&0!==this._viewportElement.scrollTop||t>0&&i0&&(s=e),r=""}}return{bufferElements:n,cursorElement:s}}getLinesScrolled(e){if(0===e.deltaY||e.shiftKey)return 0;let t=this._applyScrollModifier(e.deltaY,e);return e.deltaMode===WheelEvent.DOM_DELTA_PIXEL?(t/=this._currentRowHeight+0,this._wheelPartialScroll+=t,t=Math.floor(Math.abs(this._wheelPartialScroll))*(this._wheelPartialScroll>0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(t*=this._bufferService.rows),t}_applyScrollModifier(e,t){const i=this._optionsService.rawOptions.fastScrollModifier;return"alt"===i&&t.altKey||"ctrl"===i&&t.ctrlKey||"shift"===i&&t.shiftKey?e*this._optionsService.rawOptions.fastScrollSensitivity*this._optionsService.rawOptions.scrollSensitivity:e*this._optionsService.rawOptions.scrollSensitivity}handleTouchStart(e){this._lastTouchY=e.touches[0].pageY}handleTouchMove(e){const t=this._lastTouchY-e.touches[0].pageY;return this._lastTouchY=e.touches[0].pageY,0!==t&&(this._viewportElement.scrollTop+=t,this._bubbleScroll(e,t))}};t.Viewport=l=s([r(2,c.IBufferService),r(3,c.IOptionsService),r(4,o.ICharSizeService),r(5,o.IRenderService),r(6,o.ICoreBrowserService),r(7,o.IThemeService)],l)},3107:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferDecorationRenderer=void 0;const n=i(3656),o=i(4725),a=i(844),h=i(2585);let c=t.BufferDecorationRenderer=class extends a.Disposable{constructor(e,t,i,s){super(),this._screenElement=e,this._bufferService=t,this._decorationService=i,this._renderService=s,this._decorationElements=new Map,this._altBufferIsActive=!1,this._dimensionsChanged=!1,this._container=document.createElement("div"),this._container.classList.add("xterm-decoration-container"),this._screenElement.appendChild(this._container),this.register(this._renderService.onRenderedViewportChange((()=>this._doRefreshDecorations()))),this.register(this._renderService.onDimensionsChange((()=>{this._dimensionsChanged=!0,this._queueRefresh()}))),this.register((0,n.addDisposableDomListener)(window,"resize",(()=>this._queueRefresh()))),this.register(this._bufferService.buffers.onBufferActivate((()=>{this._altBufferIsActive=this._bufferService.buffer===this._bufferService.buffers.alt}))),this.register(this._decorationService.onDecorationRegistered((()=>this._queueRefresh()))),this.register(this._decorationService.onDecorationRemoved((e=>this._removeDecoration(e)))),this.register((0,a.toDisposable)((()=>{this._container.remove(),this._decorationElements.clear()})))}_queueRefresh(){void 0===this._animationFrame&&(this._animationFrame=this._renderService.addRefreshCallback((()=>{this._doRefreshDecorations(),this._animationFrame=void 0})))}_doRefreshDecorations(){for(const e of this._decorationService.decorations)this._renderDecoration(e);this._dimensionsChanged=!1}_renderDecoration(e){this._refreshStyle(e),this._dimensionsChanged&&this._refreshXPosition(e)}_createElement(e){var t,i;const s=document.createElement("div");s.classList.add("xterm-decoration"),s.classList.toggle("xterm-decoration-top-layer","top"===(null===(t=null==e?void 0:e.options)||void 0===t?void 0:t.layer)),s.style.width=`${Math.round((e.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,s.style.height=(e.options.height||1)*this._renderService.dimensions.css.cell.height+"px",s.style.top=(e.marker.line-this._bufferService.buffers.active.ydisp)*this._renderService.dimensions.css.cell.height+"px",s.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`;const r=null!==(i=e.options.x)&&void 0!==i?i:0;return r&&r>this._bufferService.cols&&(s.style.display="none"),this._refreshXPosition(e,s),s}_refreshStyle(e){const t=e.marker.line-this._bufferService.buffers.active.ydisp;if(t<0||t>=this._bufferService.rows)e.element&&(e.element.style.display="none",e.onRenderEmitter.fire(e.element));else{let i=this._decorationElements.get(e);i||(i=this._createElement(e),e.element=i,this._decorationElements.set(e,i),this._container.appendChild(i),e.onDispose((()=>{this._decorationElements.delete(e),i.remove()}))),i.style.top=t*this._renderService.dimensions.css.cell.height+"px",i.style.display=this._altBufferIsActive?"none":"block",e.onRenderEmitter.fire(i)}}_refreshXPosition(e,t=e.element){var i;if(!t)return;const s=null!==(i=e.options.x)&&void 0!==i?i:0;"right"===(e.options.anchor||"left")?t.style.right=s?s*this._renderService.dimensions.css.cell.width+"px":"":t.style.left=s?s*this._renderService.dimensions.css.cell.width+"px":""}_removeDecoration(e){var t;null===(t=this._decorationElements.get(e))||void 0===t||t.remove(),this._decorationElements.delete(e),e.dispose()}};t.BufferDecorationRenderer=c=s([r(1,h.IBufferService),r(2,h.IDecorationService),r(3,o.IRenderService)],c)},5871:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ColorZoneStore=void 0,t.ColorZoneStore=class{constructor(){this._zones=[],this._zonePool=[],this._zonePoolIndex=0,this._linePadding={full:0,left:0,center:0,right:0}}get zones(){return this._zonePool.length=Math.min(this._zonePool.length,this._zones.length),this._zones}clear(){this._zones.length=0,this._zonePoolIndex=0}addDecoration(e){if(e.options.overviewRulerOptions){for(const t of this._zones)if(t.color===e.options.overviewRulerOptions.color&&t.position===e.options.overviewRulerOptions.position){if(this._lineIntersectsZone(t,e.marker.line))return;if(this._lineAdjacentToZone(t,e.marker.line,e.options.overviewRulerOptions.position))return void this._addLineToZone(t,e.marker.line)}if(this._zonePoolIndex=e.startBufferLine&&t<=e.endBufferLine}_lineAdjacentToZone(e,t,i){return t>=e.startBufferLine-this._linePadding[i||"full"]&&t<=e.endBufferLine+this._linePadding[i||"full"]}_addLineToZone(e,t){e.startBufferLine=Math.min(e.startBufferLine,t),e.endBufferLine=Math.max(e.endBufferLine,t)}}},5744:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.OverviewRulerRenderer=void 0;const n=i(5871),o=i(3656),a=i(4725),h=i(844),c=i(2585),l={full:0,left:0,center:0,right:0},d={full:0,left:0,center:0,right:0},_={full:0,left:0,center:0,right:0};let u=t.OverviewRulerRenderer=class extends h.Disposable{get _width(){return this._optionsService.options.overviewRulerWidth||0}constructor(e,t,i,s,r,o,a){var c;super(),this._viewportElement=e,this._screenElement=t,this._bufferService=i,this._decorationService=s,this._renderService=r,this._optionsService=o,this._coreBrowseService=a,this._colorZoneStore=new n.ColorZoneStore,this._shouldUpdateDimensions=!0,this._shouldUpdateAnchor=!0,this._lastKnownBufferLength=0,this._canvas=document.createElement("canvas"),this._canvas.classList.add("xterm-decoration-overview-ruler"),this._refreshCanvasDimensions(),null===(c=this._viewportElement.parentElement)||void 0===c||c.insertBefore(this._canvas,this._viewportElement);const l=this._canvas.getContext("2d");if(!l)throw new Error("Ctx cannot be null");this._ctx=l,this._registerDecorationListeners(),this._registerBufferChangeListeners(),this._registerDimensionChangeListeners(),this.register((0,h.toDisposable)((()=>{var e;null===(e=this._canvas)||void 0===e||e.remove()})))}_registerDecorationListeners(){this.register(this._decorationService.onDecorationRegistered((()=>this._queueRefresh(void 0,!0)))),this.register(this._decorationService.onDecorationRemoved((()=>this._queueRefresh(void 0,!0))))}_registerBufferChangeListeners(){this.register(this._renderService.onRenderedViewportChange((()=>this._queueRefresh()))),this.register(this._bufferService.buffers.onBufferActivate((()=>{this._canvas.style.display=this._bufferService.buffer===this._bufferService.buffers.alt?"none":"block"}))),this.register(this._bufferService.onScroll((()=>{this._lastKnownBufferLength!==this._bufferService.buffers.normal.lines.length&&(this._refreshDrawHeightConstants(),this._refreshColorZonePadding())})))}_registerDimensionChangeListeners(){this.register(this._renderService.onRender((()=>{this._containerHeight&&this._containerHeight===this._screenElement.clientHeight||(this._queueRefresh(!0),this._containerHeight=this._screenElement.clientHeight)}))),this.register(this._optionsService.onSpecificOptionChange("overviewRulerWidth",(()=>this._queueRefresh(!0)))),this.register((0,o.addDisposableDomListener)(this._coreBrowseService.window,"resize",(()=>this._queueRefresh(!0)))),this._queueRefresh(!0)}_refreshDrawConstants(){const e=Math.floor(this._canvas.width/3),t=Math.ceil(this._canvas.width/3);d.full=this._canvas.width,d.left=e,d.center=t,d.right=e,this._refreshDrawHeightConstants(),_.full=0,_.left=0,_.center=d.left,_.right=d.left+d.center}_refreshDrawHeightConstants(){l.full=Math.round(2*this._coreBrowseService.dpr);const e=this._canvas.height/this._bufferService.buffer.lines.length,t=Math.round(Math.max(Math.min(e,12),6)*this._coreBrowseService.dpr);l.left=t,l.center=t,l.right=t}_refreshColorZonePadding(){this._colorZoneStore.setPadding({full:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.full),left:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.left),center:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.center),right:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.right)}),this._lastKnownBufferLength=this._bufferService.buffers.normal.lines.length}_refreshCanvasDimensions(){this._canvas.style.width=`${this._width}px`,this._canvas.width=Math.round(this._width*this._coreBrowseService.dpr),this._canvas.style.height=`${this._screenElement.clientHeight}px`,this._canvas.height=Math.round(this._screenElement.clientHeight*this._coreBrowseService.dpr),this._refreshDrawConstants(),this._refreshColorZonePadding()}_refreshDecorations(){this._shouldUpdateDimensions&&this._refreshCanvasDimensions(),this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this._colorZoneStore.clear();for(const e of this._decorationService.decorations)this._colorZoneStore.addDecoration(e);this._ctx.lineWidth=1;const e=this._colorZoneStore.zones;for(const t of e)"full"!==t.position&&this._renderColorZone(t);for(const t of e)"full"===t.position&&this._renderColorZone(t);this._shouldUpdateDimensions=!1,this._shouldUpdateAnchor=!1}_renderColorZone(e){this._ctx.fillStyle=e.color,this._ctx.fillRect(_[e.position||"full"],Math.round((this._canvas.height-1)*(e.startBufferLine/this._bufferService.buffers.active.lines.length)-l[e.position||"full"]/2),d[e.position||"full"],Math.round((this._canvas.height-1)*((e.endBufferLine-e.startBufferLine)/this._bufferService.buffers.active.lines.length)+l[e.position||"full"]))}_queueRefresh(e,t){this._shouldUpdateDimensions=e||this._shouldUpdateDimensions,this._shouldUpdateAnchor=t||this._shouldUpdateAnchor,void 0===this._animationFrame&&(this._animationFrame=this._coreBrowseService.window.requestAnimationFrame((()=>{this._refreshDecorations(),this._animationFrame=void 0})))}};t.OverviewRulerRenderer=u=s([r(2,c.IBufferService),r(3,c.IDecorationService),r(4,a.IRenderService),r(5,c.IOptionsService),r(6,a.ICoreBrowserService)],u)},2950:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CompositionHelper=void 0;const n=i(4725),o=i(2585),a=i(2584);let h=t.CompositionHelper=class{get isComposing(){return this._isComposing}constructor(e,t,i,s,r,n){this._textarea=e,this._compositionView=t,this._bufferService=i,this._optionsService=s,this._coreService=r,this._renderService=n,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0},this._dataAlreadySent=""}compositionstart(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent="",this._dataAlreadySent="",this._compositionView.classList.add("active")}compositionupdate(e){this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout((()=>{this._compositionPosition.end=this._textarea.value.length}),0)}compositionend(){this._finalizeComposition(!0)}keydown(e){if(this._isComposing||this._isSendingComposition){if(229===e.keyCode)return!1;if(16===e.keyCode||17===e.keyCode||18===e.keyCode)return!1;this._finalizeComposition(!1)}return 229!==e.keyCode||(this._handleAnyTextareaChanges(),!1)}_finalizeComposition(e){if(this._compositionView.classList.remove("active"),this._isComposing=!1,e){const e={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout((()=>{if(this._isSendingComposition){let t;this._isSendingComposition=!1,e.start+=this._dataAlreadySent.length,t=this._isComposing?this._textarea.value.substring(e.start,e.end):this._textarea.value.substring(e.start),t.length>0&&this._coreService.triggerDataEvent(t,!0)}}),0)}else{this._isSendingComposition=!1;const e=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(e,!0)}}_handleAnyTextareaChanges(){const e=this._textarea.value;setTimeout((()=>{if(!this._isComposing){const t=this._textarea.value,i=t.replace(e,"");this._dataAlreadySent=i,t.length>e.length?this._coreService.triggerDataEvent(i,!0):t.lengththis.updateCompositionElements(!0)),0)}}};t.CompositionHelper=h=s([r(2,o.IBufferService),r(3,o.IOptionsService),r(4,o.ICoreService),r(5,n.IRenderService)],h)},9806:(e,t)=>{function i(e,t,i){const s=i.getBoundingClientRect(),r=e.getComputedStyle(i),n=parseInt(r.getPropertyValue("padding-left")),o=parseInt(r.getPropertyValue("padding-top"));return[t.clientX-s.left-n,t.clientY-s.top-o]}Object.defineProperty(t,"__esModule",{value:!0}),t.getCoords=t.getCoordsRelativeToElement=void 0,t.getCoordsRelativeToElement=i,t.getCoords=function(e,t,s,r,n,o,a,h,c){if(!o)return;const l=i(e,t,s);return l?(l[0]=Math.ceil((l[0]+(c?a/2:0))/a),l[1]=Math.ceil(l[1]/h),l[0]=Math.min(Math.max(l[0],1),r+(c?1:0)),l[1]=Math.min(Math.max(l[1],1),n),l):void 0}},9504:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.moveToCellSequence=void 0;const s=i(2584);function r(e,t,i,s){const r=e-n(e,i),a=t-n(t,i),l=Math.abs(r-a)-function(e,t,i){let s=0;const r=e-n(e,i),a=t-n(t,i);for(let n=0;n=0&&et?"A":"B"}function a(e,t,i,s,r,n){let o=e,a=t,h="";for(;o!==i||a!==s;)o+=r?1:-1,r&&o>n.cols-1?(h+=n.buffer.translateBufferLineToString(a,!1,e,o),o=0,e=0,a++):!r&&o<0&&(h+=n.buffer.translateBufferLineToString(a,!1,0,e+1),o=n.cols-1,e=o,a--);return h+n.buffer.translateBufferLineToString(a,!1,e,o)}function h(e,t){const i=t?"O":"[";return s.C0.ESC+i+e}function c(e,t){e=Math.floor(e);let i="";for(let s=0;s0?s-n(s,o):t;const _=s,u=function(e,t,i,s,o,a){let h;return h=r(i,s,o,a).length>0?s-n(s,o):t,e=i&&he?"D":"C",c(Math.abs(o-e),h(d,s));d=l>t?"D":"C";const _=Math.abs(l-t);return c(function(e,t){return t.cols-e}(l>t?e:o,i)+(_-1)*i.cols+1+((l>t?o:e)-1),h(d,s))}},1296:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DomRenderer=void 0;const n=i(3787),o=i(2550),a=i(2223),h=i(6171),c=i(4725),l=i(8055),d=i(8460),_=i(844),u=i(2585),f="xterm-dom-renderer-owner-",v="xterm-rows",p="xterm-fg-",g="xterm-bg-",m="xterm-focus",S="xterm-selection";let C=1,b=t.DomRenderer=class extends _.Disposable{constructor(e,t,i,s,r,a,c,l,u,p){super(),this._element=e,this._screenElement=t,this._viewportElement=i,this._linkifier2=s,this._charSizeService=a,this._optionsService=c,this._bufferService=l,this._coreBrowserService=u,this._themeService=p,this._terminalClass=C++,this._rowElements=[],this.onRequestRedraw=this.register(new d.EventEmitter).event,this._rowContainer=document.createElement("div"),this._rowContainer.classList.add(v),this._rowContainer.style.lineHeight="normal",this._rowContainer.setAttribute("aria-hidden","true"),this._refreshRowElements(this._bufferService.cols,this._bufferService.rows),this._selectionContainer=document.createElement("div"),this._selectionContainer.classList.add(S),this._selectionContainer.setAttribute("aria-hidden","true"),this.dimensions=(0,h.createRenderDimensions)(),this._updateDimensions(),this.register(this._optionsService.onOptionChange((()=>this._handleOptionsChanged()))),this.register(this._themeService.onChangeColors((e=>this._injectCss(e)))),this._injectCss(this._themeService.colors),this._rowFactory=r.createInstance(n.DomRendererRowFactory,document),this._element.classList.add(f+this._terminalClass),this._screenElement.appendChild(this._rowContainer),this._screenElement.appendChild(this._selectionContainer),this.register(this._linkifier2.onShowLinkUnderline((e=>this._handleLinkHover(e)))),this.register(this._linkifier2.onHideLinkUnderline((e=>this._handleLinkLeave(e)))),this.register((0,_.toDisposable)((()=>{this._element.classList.remove(f+this._terminalClass),this._rowContainer.remove(),this._selectionContainer.remove(),this._widthCache.dispose(),this._themeStyleElement.remove(),this._dimensionsStyleElement.remove()}))),this._widthCache=new o.WidthCache(document),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}_updateDimensions(){const e=this._coreBrowserService.dpr;this.dimensions.device.char.width=this._charSizeService.width*e,this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*e),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.left=0,this.dimensions.device.char.top=0,this.dimensions.device.canvas.width=this.dimensions.device.cell.width*this._bufferService.cols,this.dimensions.device.canvas.height=this.dimensions.device.cell.height*this._bufferService.rows,this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/e),this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/e),this.dimensions.css.cell.width=this.dimensions.css.canvas.width/this._bufferService.cols,this.dimensions.css.cell.height=this.dimensions.css.canvas.height/this._bufferService.rows;for(const e of this._rowElements)e.style.width=`${this.dimensions.css.canvas.width}px`,e.style.height=`${this.dimensions.css.cell.height}px`,e.style.lineHeight=`${this.dimensions.css.cell.height}px`,e.style.overflow="hidden";this._dimensionsStyleElement||(this._dimensionsStyleElement=document.createElement("style"),this._screenElement.appendChild(this._dimensionsStyleElement));const t=`${this._terminalSelector} .${v} span { display: inline-block; height: 100%; vertical-align: top;}`;this._dimensionsStyleElement.textContent=t,this._selectionContainer.style.height=this._viewportElement.style.height,this._screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._screenElement.style.height=`${this.dimensions.css.canvas.height}px`}_injectCss(e){this._themeStyleElement||(this._themeStyleElement=document.createElement("style"),this._screenElement.appendChild(this._themeStyleElement));let t=`${this._terminalSelector} .${v} { color: ${e.foreground.css}; font-family: ${this._optionsService.rawOptions.fontFamily}; font-size: ${this._optionsService.rawOptions.fontSize}px; font-kerning: none; white-space: pre}`;t+=`${this._terminalSelector} .${v} .xterm-dim { color: ${l.color.multiplyOpacity(e.foreground,.5).css};}`,t+=`${this._terminalSelector} span:not(.xterm-bold) { font-weight: ${this._optionsService.rawOptions.fontWeight};}${this._terminalSelector} span.xterm-bold { font-weight: ${this._optionsService.rawOptions.fontWeightBold};}${this._terminalSelector} span.xterm-italic { font-style: italic;}`,t+="@keyframes blink_box_shadow_"+this._terminalClass+" { 50% { border-bottom-style: hidden; }}",t+="@keyframes blink_block_"+this._terminalClass+" { 0% {"+` background-color: ${e.cursor.css};`+` color: ${e.cursorAccent.css}; } 50% { background-color: inherit;`+` color: ${e.cursor.css}; }}`,t+=`${this._terminalSelector} .${v}.${m} .xterm-cursor.xterm-cursor-blink:not(.xterm-cursor-block) { animation: blink_box_shadow_`+this._terminalClass+" 1s step-end infinite;}"+`${this._terminalSelector} .${v}.${m} .xterm-cursor.xterm-cursor-blink.xterm-cursor-block { animation: blink_block_`+this._terminalClass+" 1s step-end infinite;}"+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-block {`+` background-color: ${e.cursor.css};`+` color: ${e.cursorAccent.css};}`+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-outline {`+` outline: 1px solid ${e.cursor.css}; outline-offset: -1px;}`+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-bar {`+` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${e.cursor.css} inset;}`+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-underline {`+` border-bottom: 1px ${e.cursor.css}; border-bottom-style: solid; height: calc(100% - 1px);}`,t+=`${this._terminalSelector} .${S} { position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;}${this._terminalSelector}.focus .${S} div { position: absolute; background-color: ${e.selectionBackgroundOpaque.css};}${this._terminalSelector} .${S} div { position: absolute; background-color: ${e.selectionInactiveBackgroundOpaque.css};}`;for(const[i,s]of e.ansi.entries())t+=`${this._terminalSelector} .${p}${i} { color: ${s.css}; }${this._terminalSelector} .${p}${i}.xterm-dim { color: ${l.color.multiplyOpacity(s,.5).css}; }${this._terminalSelector} .${g}${i} { background-color: ${s.css}; }`;t+=`${this._terminalSelector} .${p}${a.INVERTED_DEFAULT_COLOR} { color: ${l.color.opaque(e.background).css}; }${this._terminalSelector} .${p}${a.INVERTED_DEFAULT_COLOR}.xterm-dim { color: ${l.color.multiplyOpacity(l.color.opaque(e.background),.5).css}; }${this._terminalSelector} .${g}${a.INVERTED_DEFAULT_COLOR} { background-color: ${e.foreground.css}; }`,this._themeStyleElement.textContent=t}_setDefaultSpacing(){const e=this.dimensions.css.cell.width-this._widthCache.get("W",!1,!1);this._rowContainer.style.letterSpacing=`${e}px`,this._rowFactory.defaultSpacing=e}handleDevicePixelRatioChange(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}_refreshRowElements(e,t){for(let e=this._rowElements.length;e<=t;e++){const e=document.createElement("div");this._rowContainer.appendChild(e),this._rowElements.push(e)}for(;this._rowElements.length>t;)this._rowContainer.removeChild(this._rowElements.pop())}handleResize(e,t){this._refreshRowElements(e,t),this._updateDimensions()}handleCharSizeChanged(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}handleBlur(){this._rowContainer.classList.remove(m)}handleFocus(){this._rowContainer.classList.add(m),this.renderRows(this._bufferService.buffer.y,this._bufferService.buffer.y)}handleSelectionChanged(e,t,i){if(this._selectionContainer.replaceChildren(),this._rowFactory.handleSelectionChanged(e,t,i),this.renderRows(0,this._bufferService.rows-1),!e||!t)return;const s=e[1]-this._bufferService.buffer.ydisp,r=t[1]-this._bufferService.buffer.ydisp,n=Math.max(s,0),o=Math.min(r,this._bufferService.rows-1);if(n>=this._bufferService.rows||o<0)return;const a=document.createDocumentFragment();if(i){const i=e[0]>t[0];a.appendChild(this._createSelectionElement(n,i?t[0]:e[0],i?e[0]:t[0],o-n+1))}else{const i=s===n?e[0]:0,h=n===r?t[0]:this._bufferService.cols;a.appendChild(this._createSelectionElement(n,i,h));const c=o-n-1;if(a.appendChild(this._createSelectionElement(n+1,0,this._bufferService.cols,c)),n!==o){const e=r===o?t[0]:this._bufferService.cols;a.appendChild(this._createSelectionElement(o,0,e))}}this._selectionContainer.appendChild(a)}_createSelectionElement(e,t,i,s=1){const r=document.createElement("div");return r.style.height=s*this.dimensions.css.cell.height+"px",r.style.top=e*this.dimensions.css.cell.height+"px",r.style.left=t*this.dimensions.css.cell.width+"px",r.style.width=this.dimensions.css.cell.width*(i-t)+"px",r}handleCursorMove(){}_handleOptionsChanged(){this._updateDimensions(),this._injectCss(this._themeService.colors),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}clear(){for(const e of this._rowElements)e.replaceChildren()}renderRows(e,t){const i=this._bufferService.buffer,s=i.ybase+i.y,r=Math.min(i.x,this._bufferService.cols-1),n=this._optionsService.rawOptions.cursorBlink,o=this._optionsService.rawOptions.cursorStyle,a=this._optionsService.rawOptions.cursorInactiveStyle;for(let h=e;h<=t;h++){const e=h+i.ydisp,t=this._rowElements[h],c=i.lines.get(e);if(!t||!c)break;t.replaceChildren(...this._rowFactory.createRow(c,e,e===s,o,a,r,n,this.dimensions.css.cell.width,this._widthCache,-1,-1))}}get _terminalSelector(){return`.${f}${this._terminalClass}`}_handleLinkHover(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!0)}_handleLinkLeave(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!1)}_setCellUnderline(e,t,i,s,r,n){i<0&&(e=0),s<0&&(t=0);const o=this._bufferService.rows-1;i=Math.max(Math.min(i,o),0),s=Math.max(Math.min(s,o),0),r=Math.min(r,this._bufferService.cols);const a=this._bufferService.buffer,h=a.ybase+a.y,c=Math.min(a.x,r-1),l=this._optionsService.rawOptions.cursorBlink,d=this._optionsService.rawOptions.cursorStyle,_=this._optionsService.rawOptions.cursorInactiveStyle;for(let o=i;o<=s;++o){const u=o+a.ydisp,f=this._rowElements[o],v=a.lines.get(u);if(!f||!v)break;f.replaceChildren(...this._rowFactory.createRow(v,u,u===h,d,_,c,l,this.dimensions.css.cell.width,this._widthCache,n?o===i?e:0:-1,n?(o===s?t:r)-1:-1))}}};t.DomRenderer=b=s([r(4,u.IInstantiationService),r(5,c.ICharSizeService),r(6,u.IOptionsService),r(7,u.IBufferService),r(8,c.ICoreBrowserService),r(9,c.IThemeService)],b)},3787:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DomRendererRowFactory=void 0;const n=i(2223),o=i(643),a=i(511),h=i(2585),c=i(8055),l=i(4725),d=i(4269),_=i(6171),u=i(3734);let f=t.DomRendererRowFactory=class{constructor(e,t,i,s,r,n,o){this._document=e,this._characterJoinerService=t,this._optionsService=i,this._coreBrowserService=s,this._coreService=r,this._decorationService=n,this._themeService=o,this._workCell=new a.CellData,this._columnSelectMode=!1,this.defaultSpacing=0}handleSelectionChanged(e,t,i){this._selectionStart=e,this._selectionEnd=t,this._columnSelectMode=i}createRow(e,t,i,s,r,a,h,l,_,f,p){const g=[],m=this._characterJoinerService.getJoinedCharacters(t),S=this._themeService.colors;let C,b=e.getNoBgTrimmedLength();i&&b0&&M===m[0][0]){O=!0;const t=m.shift();I=new d.JoinedCellData(this._workCell,e.translateToString(!0,t[0],t[1]),t[1]-t[0]),P=t[1]-1,b=I.getWidth()}const H=this._isCellInSelection(M,t),F=i&&M===a,W=T&&M>=f&&M<=p;let U=!1;this._decorationService.forEachDecorationAtCell(M,t,void 0,(e=>{U=!0}));let N=I.getChars()||o.WHITESPACE_CELL_CHAR;if(" "===N&&(I.isUnderline()||I.isOverline())&&(N=" "),A=b*l-_.get(N,I.isBold(),I.isItalic()),C){if(y&&(H&&x||!H&&!x&&I.bg===E)&&(H&&x&&S.selectionForeground||I.fg===k)&&I.extended.ext===L&&W===D&&A===R&&!F&&!O&&!U){w+=N,y++;continue}y&&(C.textContent=w),C=this._document.createElement("span"),y=0,w=""}else C=this._document.createElement("span");if(E=I.bg,k=I.fg,L=I.extended.ext,D=W,R=A,x=H,O&&a>=M&&a<=P&&(a=M),!this._coreService.isCursorHidden&&F)if(B.push("xterm-cursor"),this._coreBrowserService.isFocused)h&&B.push("xterm-cursor-blink"),B.push("bar"===s?"xterm-cursor-bar":"underline"===s?"xterm-cursor-underline":"xterm-cursor-block");else if(r)switch(r){case"outline":B.push("xterm-cursor-outline");break;case"block":B.push("xterm-cursor-block");break;case"bar":B.push("xterm-cursor-bar");break;case"underline":B.push("xterm-cursor-underline")}if(I.isBold()&&B.push("xterm-bold"),I.isItalic()&&B.push("xterm-italic"),I.isDim()&&B.push("xterm-dim"),w=I.isInvisible()?o.WHITESPACE_CELL_CHAR:I.getChars()||o.WHITESPACE_CELL_CHAR,I.isUnderline()&&(B.push(`xterm-underline-${I.extended.underlineStyle}`)," "===w&&(w=" "),!I.isUnderlineColorDefault()))if(I.isUnderlineColorRGB())C.style.textDecorationColor=`rgb(${u.AttributeData.toColorRGB(I.getUnderlineColor()).join(",")})`;else{let e=I.getUnderlineColor();this._optionsService.rawOptions.drawBoldTextInBrightColors&&I.isBold()&&e<8&&(e+=8),C.style.textDecorationColor=S.ansi[e].css}I.isOverline()&&(B.push("xterm-overline")," "===w&&(w=" ")),I.isStrikethrough()&&B.push("xterm-strikethrough"),W&&(C.style.textDecoration="underline");let $=I.getFgColor(),j=I.getFgColorMode(),z=I.getBgColor(),K=I.getBgColorMode();const q=!!I.isInverse();if(q){const e=$;$=z,z=e;const t=j;j=K,K=t}let V,G,X,J=!1;switch(this._decorationService.forEachDecorationAtCell(M,t,void 0,(e=>{"top"!==e.options.layer&&J||(e.backgroundColorRGB&&(K=50331648,z=e.backgroundColorRGB.rgba>>8&16777215,V=e.backgroundColorRGB),e.foregroundColorRGB&&(j=50331648,$=e.foregroundColorRGB.rgba>>8&16777215,G=e.foregroundColorRGB),J="top"===e.options.layer)})),!J&&H&&(V=this._coreBrowserService.isFocused?S.selectionBackgroundOpaque:S.selectionInactiveBackgroundOpaque,z=V.rgba>>8&16777215,K=50331648,J=!0,S.selectionForeground&&(j=50331648,$=S.selectionForeground.rgba>>8&16777215,G=S.selectionForeground)),J&&B.push("xterm-decoration-top"),K){case 16777216:case 33554432:X=S.ansi[z],B.push(`xterm-bg-${z}`);break;case 50331648:X=c.rgba.toColor(z>>16,z>>8&255,255&z),this._addStyle(C,`background-color:#${v((z>>>0).toString(16),"0",6)}`);break;default:q?(X=S.foreground,B.push(`xterm-bg-${n.INVERTED_DEFAULT_COLOR}`)):X=S.background}switch(V||I.isDim()&&(V=c.color.multiplyOpacity(X,.5)),j){case 16777216:case 33554432:I.isBold()&&$<8&&this._optionsService.rawOptions.drawBoldTextInBrightColors&&($+=8),this._applyMinimumContrast(C,X,S.ansi[$],I,V,void 0)||B.push(`xterm-fg-${$}`);break;case 50331648:const e=c.rgba.toColor($>>16&255,$>>8&255,255&$);this._applyMinimumContrast(C,X,e,I,V,G)||this._addStyle(C,`color:#${v($.toString(16),"0",6)}`);break;default:this._applyMinimumContrast(C,X,S.foreground,I,V,void 0)||q&&B.push(`xterm-fg-${n.INVERTED_DEFAULT_COLOR}`)}B.length&&(C.className=B.join(" "),B.length=0),F||O||U?C.textContent=w:y++,A!==this.defaultSpacing&&(C.style.letterSpacing=`${A}px`),g.push(C),M=P}return C&&y&&(C.textContent=w),g}_applyMinimumContrast(e,t,i,s,r,n){if(1===this._optionsService.rawOptions.minimumContrastRatio||(0,_.excludeFromContrastRatioDemands)(s.getCode()))return!1;const o=this._getContrastCache(s);let a;if(r||n||(a=o.getColor(t.rgba,i.rgba)),void 0===a){const e=this._optionsService.rawOptions.minimumContrastRatio/(s.isDim()?2:1);a=c.color.ensureContrastRatio(r||t,n||i,e),o.setColor((r||t).rgba,(n||i).rgba,null!=a?a:null)}return!!a&&(this._addStyle(e,`color:${a.css}`),!0)}_getContrastCache(e){return e.isDim()?this._themeService.colors.halfContrastCache:this._themeService.colors.contrastCache}_addStyle(e,t){e.setAttribute("style",`${e.getAttribute("style")||""}${t};`)}_isCellInSelection(e,t){const i=this._selectionStart,s=this._selectionEnd;return!(!i||!s)&&(this._columnSelectMode?i[0]<=s[0]?e>=i[0]&&t>=i[1]&&e=i[1]&&e>=s[0]&&t<=s[1]:t>i[1]&&t=i[0]&&e=i[0])}};function v(e,t,i){for(;e.length{Object.defineProperty(t,"__esModule",{value:!0}),t.WidthCache=void 0,t.WidthCache=class{constructor(e){this._flat=new Float32Array(256),this._font="",this._fontSize=0,this._weight="normal",this._weightBold="bold",this._measureElements=[],this._container=e.createElement("div"),this._container.style.position="absolute",this._container.style.top="-50000px",this._container.style.width="50000px",this._container.style.whiteSpace="pre",this._container.style.fontKerning="none";const t=e.createElement("span"),i=e.createElement("span");i.style.fontWeight="bold";const s=e.createElement("span");s.style.fontStyle="italic";const r=e.createElement("span");r.style.fontWeight="bold",r.style.fontStyle="italic",this._measureElements=[t,i,s,r],this._container.appendChild(t),this._container.appendChild(i),this._container.appendChild(s),this._container.appendChild(r),e.body.appendChild(this._container),this.clear()}dispose(){this._container.remove(),this._measureElements.length=0,this._holey=void 0}clear(){this._flat.fill(-9999),this._holey=new Map}setFont(e,t,i,s){e===this._font&&t===this._fontSize&&i===this._weight&&s===this._weightBold||(this._font=e,this._fontSize=t,this._weight=i,this._weightBold=s,this._container.style.fontFamily=this._font,this._container.style.fontSize=`${this._fontSize}px`,this._measureElements[0].style.fontWeight=`${i}`,this._measureElements[1].style.fontWeight=`${s}`,this._measureElements[2].style.fontWeight=`${i}`,this._measureElements[3].style.fontWeight=`${s}`,this.clear())}get(e,t,i){let s=0;if(!t&&!i&&1===e.length&&(s=e.charCodeAt(0))<256)return-9999!==this._flat[s]?this._flat[s]:this._flat[s]=this._measure(e,0);let r=e;t&&(r+="B"),i&&(r+="I");let n=this._holey.get(r);if(void 0===n){let s=0;t&&(s|=1),i&&(s|=2),n=this._measure(e,s),this._holey.set(r,n)}return n}_measure(e,t){const i=this._measureElements[t];return i.textContent=e.repeat(32),i.offsetWidth/32}}},2223:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.TEXT_BASELINE=t.DIM_OPACITY=t.INVERTED_DEFAULT_COLOR=void 0;const s=i(6114);t.INVERTED_DEFAULT_COLOR=257,t.DIM_OPACITY=.5,t.TEXT_BASELINE=s.isFirefox||s.isLegacyEdge?"bottom":"ideographic"},6171:(e,t)=>{function i(e){return 57508<=e&&e<=57558}Object.defineProperty(t,"__esModule",{value:!0}),t.createRenderDimensions=t.excludeFromContrastRatioDemands=t.isRestrictedPowerlineGlyph=t.isPowerlineGlyph=t.throwIfFalsy=void 0,t.throwIfFalsy=function(e){if(!e)throw new Error("value must not be falsy");return e},t.isPowerlineGlyph=i,t.isRestrictedPowerlineGlyph=function(e){return 57520<=e&&e<=57527},t.excludeFromContrastRatioDemands=function(e){return i(e)||function(e){return 9472<=e&&e<=9631}(e)},t.createRenderDimensions=function(){return{css:{canvas:{width:0,height:0},cell:{width:0,height:0}},device:{canvas:{width:0,height:0},cell:{width:0,height:0},char:{width:0,height:0,left:0,top:0}}}}},456:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionModel=void 0,t.SelectionModel=class{constructor(e){this._bufferService=e,this.isSelectAllActive=!1,this.selectionStartLength=0}clearSelection(){this.selectionStart=void 0,this.selectionEnd=void 0,this.isSelectAllActive=!1,this.selectionStartLength=0}get finalSelectionStart(){return this.isSelectAllActive?[0,0]:this.selectionEnd&&this.selectionStart&&this.areSelectionValuesReversed()?this.selectionEnd:this.selectionStart}get finalSelectionEnd(){if(this.isSelectAllActive)return[this._bufferService.cols,this._bufferService.buffer.ybase+this._bufferService.rows-1];if(this.selectionStart){if(!this.selectionEnd||this.areSelectionValuesReversed()){const e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?e%this._bufferService.cols==0?[this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)-1]:[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}if(this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]){const e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[Math.max(e,this.selectionEnd[0]),this.selectionEnd[1]]}return this.selectionEnd}}areSelectionValuesReversed(){const e=this.selectionStart,t=this.selectionEnd;return!(!e||!t)&&(e[1]>t[1]||e[1]===t[1]&&e[0]>t[0])}handleTrim(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)}}},428:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CharSizeService=void 0;const n=i(2585),o=i(8460),a=i(844);let h=t.CharSizeService=class extends a.Disposable{get hasValidSize(){return this.width>0&&this.height>0}constructor(e,t,i){super(),this._optionsService=i,this.width=0,this.height=0,this._onCharSizeChange=this.register(new o.EventEmitter),this.onCharSizeChange=this._onCharSizeChange.event,this._measureStrategy=new c(e,t,this._optionsService),this.register(this._optionsService.onMultipleOptionChange(["fontFamily","fontSize"],(()=>this.measure())))}measure(){const e=this._measureStrategy.measure();e.width===this.width&&e.height===this.height||(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())}};t.CharSizeService=h=s([r(2,n.IOptionsService)],h);class c{constructor(e,t,i){this._document=e,this._parentElement=t,this._optionsService=i,this._result={width:0,height:0},this._measureElement=this._document.createElement("span"),this._measureElement.classList.add("xterm-char-measure-element"),this._measureElement.textContent="W".repeat(32),this._measureElement.setAttribute("aria-hidden","true"),this._measureElement.style.whiteSpace="pre",this._measureElement.style.fontKerning="none",this._parentElement.appendChild(this._measureElement)}measure(){this._measureElement.style.fontFamily=this._optionsService.rawOptions.fontFamily,this._measureElement.style.fontSize=`${this._optionsService.rawOptions.fontSize}px`;const e={height:Number(this._measureElement.offsetHeight),width:Number(this._measureElement.offsetWidth)};return 0!==e.width&&0!==e.height&&(this._result.width=e.width/32,this._result.height=Math.ceil(e.height)),this._result}}},4269:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CharacterJoinerService=t.JoinedCellData=void 0;const n=i(3734),o=i(643),a=i(511),h=i(2585);class c extends n.AttributeData{constructor(e,t,i){super(),this.content=0,this.combinedData="",this.fg=e.fg,this.bg=e.bg,this.combinedData=t,this._width=i}isCombined(){return 2097152}getWidth(){return this._width}getChars(){return this.combinedData}getCode(){return 2097151}setFromCharData(e){throw new Error("not implemented")}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}t.JoinedCellData=c;let l=t.CharacterJoinerService=class e{constructor(e){this._bufferService=e,this._characterJoiners=[],this._nextCharacterJoinerId=0,this._workCell=new a.CellData}register(e){const t={id:this._nextCharacterJoinerId++,handler:e};return this._characterJoiners.push(t),t.id}deregister(e){for(let t=0;t1){const e=this._getJoinedRanges(s,a,n,t,r);for(let t=0;t1){const e=this._getJoinedRanges(s,a,n,t,r);for(let t=0;t{Object.defineProperty(t,"__esModule",{value:!0}),t.CoreBrowserService=void 0,t.CoreBrowserService=class{constructor(e,t){this._textarea=e,this.window=t,this._isFocused=!1,this._cachedIsFocused=void 0,this._textarea.addEventListener("focus",(()=>this._isFocused=!0)),this._textarea.addEventListener("blur",(()=>this._isFocused=!1))}get dpr(){return this.window.devicePixelRatio}get isFocused(){return void 0===this._cachedIsFocused&&(this._cachedIsFocused=this._isFocused&&this._textarea.ownerDocument.hasFocus(),queueMicrotask((()=>this._cachedIsFocused=void 0))),this._cachedIsFocused}}},8934:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseService=void 0;const n=i(4725),o=i(9806);let a=t.MouseService=class{constructor(e,t){this._renderService=e,this._charSizeService=t}getCoords(e,t,i,s,r){return(0,o.getCoords)(window,e,t,i,s,this._charSizeService.hasValidSize,this._renderService.dimensions.css.cell.width,this._renderService.dimensions.css.cell.height,r)}getMouseReportCoords(e,t){const i=(0,o.getCoordsRelativeToElement)(window,e,t);if(this._charSizeService.hasValidSize)return i[0]=Math.min(Math.max(i[0],0),this._renderService.dimensions.css.canvas.width-1),i[1]=Math.min(Math.max(i[1],0),this._renderService.dimensions.css.canvas.height-1),{col:Math.floor(i[0]/this._renderService.dimensions.css.cell.width),row:Math.floor(i[1]/this._renderService.dimensions.css.cell.height),x:Math.floor(i[0]),y:Math.floor(i[1])}}};t.MouseService=a=s([r(0,n.IRenderService),r(1,n.ICharSizeService)],a)},3230:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.RenderService=void 0;const n=i(3656),o=i(6193),a=i(5596),h=i(4725),c=i(8460),l=i(844),d=i(7226),_=i(2585);let u=t.RenderService=class extends l.Disposable{get dimensions(){return this._renderer.value.dimensions}constructor(e,t,i,s,r,h,_,u){if(super(),this._rowCount=e,this._charSizeService=s,this._renderer=this.register(new l.MutableDisposable),this._pausedResizeTask=new d.DebouncedIdleTask,this._isPaused=!1,this._needsFullRefresh=!1,this._isNextRenderRedrawOnly=!0,this._needsSelectionRefresh=!1,this._canvasWidth=0,this._canvasHeight=0,this._selectionState={start:void 0,end:void 0,columnSelectMode:!1},this._onDimensionsChange=this.register(new c.EventEmitter),this.onDimensionsChange=this._onDimensionsChange.event,this._onRenderedViewportChange=this.register(new c.EventEmitter),this.onRenderedViewportChange=this._onRenderedViewportChange.event,this._onRender=this.register(new c.EventEmitter),this.onRender=this._onRender.event,this._onRefreshRequest=this.register(new c.EventEmitter),this.onRefreshRequest=this._onRefreshRequest.event,this._renderDebouncer=new o.RenderDebouncer(_.window,((e,t)=>this._renderRows(e,t))),this.register(this._renderDebouncer),this._screenDprMonitor=new a.ScreenDprMonitor(_.window),this._screenDprMonitor.setListener((()=>this.handleDevicePixelRatioChange())),this.register(this._screenDprMonitor),this.register(h.onResize((()=>this._fullRefresh()))),this.register(h.buffers.onBufferActivate((()=>{var e;return null===(e=this._renderer.value)||void 0===e?void 0:e.clear()}))),this.register(i.onOptionChange((()=>this._handleOptionsChanged()))),this.register(this._charSizeService.onCharSizeChange((()=>this.handleCharSizeChanged()))),this.register(r.onDecorationRegistered((()=>this._fullRefresh()))),this.register(r.onDecorationRemoved((()=>this._fullRefresh()))),this.register(i.onMultipleOptionChange(["customGlyphs","drawBoldTextInBrightColors","letterSpacing","lineHeight","fontFamily","fontSize","fontWeight","fontWeightBold","minimumContrastRatio"],(()=>{this.clear(),this.handleResize(h.cols,h.rows),this._fullRefresh()}))),this.register(i.onMultipleOptionChange(["cursorBlink","cursorStyle"],(()=>this.refreshRows(h.buffer.y,h.buffer.y,!0)))),this.register((0,n.addDisposableDomListener)(_.window,"resize",(()=>this.handleDevicePixelRatioChange()))),this.register(u.onChangeColors((()=>this._fullRefresh()))),"IntersectionObserver"in _.window){const e=new _.window.IntersectionObserver((e=>this._handleIntersectionChange(e[e.length-1])),{threshold:0});e.observe(t),this.register({dispose:()=>e.disconnect()})}}_handleIntersectionChange(e){this._isPaused=void 0===e.isIntersecting?0===e.intersectionRatio:!e.isIntersecting,this._isPaused||this._charSizeService.hasValidSize||this._charSizeService.measure(),!this._isPaused&&this._needsFullRefresh&&(this._pausedResizeTask.flush(),this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)}refreshRows(e,t,i=!1){this._isPaused?this._needsFullRefresh=!0:(i||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount))}_renderRows(e,t){this._renderer.value&&(e=Math.min(e,this._rowCount-1),t=Math.min(t,this._rowCount-1),this._renderer.value.renderRows(e,t),this._needsSelectionRefresh&&(this._renderer.value.handleSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),this._needsSelectionRefresh=!1),this._isNextRenderRedrawOnly||this._onRenderedViewportChange.fire({start:e,end:t}),this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0)}resize(e,t){this._rowCount=t,this._fireOnCanvasResize()}_handleOptionsChanged(){this._renderer.value&&(this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize())}_fireOnCanvasResize(){this._renderer.value&&(this._renderer.value.dimensions.css.canvas.width===this._canvasWidth&&this._renderer.value.dimensions.css.canvas.height===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.value.dimensions))}hasRenderer(){return!!this._renderer.value}setRenderer(e){this._renderer.value=e,this._renderer.value.onRequestRedraw((e=>this.refreshRows(e.start,e.end,!0))),this._needsSelectionRefresh=!0,this._fullRefresh()}addRefreshCallback(e){return this._renderDebouncer.addRefreshCallback(e)}_fullRefresh(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)}clearTextureAtlas(){var e,t;this._renderer.value&&(null===(t=(e=this._renderer.value).clearTextureAtlas)||void 0===t||t.call(e),this._fullRefresh())}handleDevicePixelRatioChange(){this._charSizeService.measure(),this._renderer.value&&(this._renderer.value.handleDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1))}handleResize(e,t){this._renderer.value&&(this._isPaused?this._pausedResizeTask.set((()=>this._renderer.value.handleResize(e,t))):this._renderer.value.handleResize(e,t),this._fullRefresh())}handleCharSizeChanged(){var e;null===(e=this._renderer.value)||void 0===e||e.handleCharSizeChanged()}handleBlur(){var e;null===(e=this._renderer.value)||void 0===e||e.handleBlur()}handleFocus(){var e;null===(e=this._renderer.value)||void 0===e||e.handleFocus()}handleSelectionChanged(e,t,i){var s;this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=i,null===(s=this._renderer.value)||void 0===s||s.handleSelectionChanged(e,t,i)}handleCursorMove(){var e;null===(e=this._renderer.value)||void 0===e||e.handleCursorMove()}clear(){var e;null===(e=this._renderer.value)||void 0===e||e.clear()}};t.RenderService=u=s([r(2,_.IOptionsService),r(3,h.ICharSizeService),r(4,_.IDecorationService),r(5,_.IBufferService),r(6,h.ICoreBrowserService),r(7,h.IThemeService)],u)},9312:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionService=void 0;const n=i(9806),o=i(9504),a=i(456),h=i(4725),c=i(8460),l=i(844),d=i(6114),_=i(4841),u=i(511),f=i(2585),v=String.fromCharCode(160),p=new RegExp(v,"g");let g=t.SelectionService=class extends l.Disposable{constructor(e,t,i,s,r,n,o,h,d){super(),this._element=e,this._screenElement=t,this._linkifier=i,this._bufferService=s,this._coreService=r,this._mouseService=n,this._optionsService=o,this._renderService=h,this._coreBrowserService=d,this._dragScrollAmount=0,this._enabled=!0,this._workCell=new u.CellData,this._mouseDownTimeStamp=0,this._oldHasSelection=!1,this._oldSelectionStart=void 0,this._oldSelectionEnd=void 0,this._onLinuxMouseSelection=this.register(new c.EventEmitter),this.onLinuxMouseSelection=this._onLinuxMouseSelection.event,this._onRedrawRequest=this.register(new c.EventEmitter),this.onRequestRedraw=this._onRedrawRequest.event,this._onSelectionChange=this.register(new c.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onRequestScrollLines=this.register(new c.EventEmitter),this.onRequestScrollLines=this._onRequestScrollLines.event,this._mouseMoveListener=e=>this._handleMouseMove(e),this._mouseUpListener=e=>this._handleMouseUp(e),this._coreService.onUserInput((()=>{this.hasSelection&&this.clearSelection()})),this._trimListener=this._bufferService.buffer.lines.onTrim((e=>this._handleTrim(e))),this.register(this._bufferService.buffers.onBufferActivate((e=>this._handleBufferActivate(e)))),this.enable(),this._model=new a.SelectionModel(this._bufferService),this._activeSelectionMode=0,this.register((0,l.toDisposable)((()=>{this._removeMouseDownListeners()})))}reset(){this.clearSelection()}disable(){this.clearSelection(),this._enabled=!1}enable(){this._enabled=!0}get selectionStart(){return this._model.finalSelectionStart}get selectionEnd(){return this._model.finalSelectionEnd}get hasSelection(){const e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!(!e||!t||e[0]===t[0]&&e[1]===t[1])}get selectionText(){const e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return"";const i=this._bufferService.buffer,s=[];if(3===this._activeSelectionMode){if(e[0]===t[0])return"";const r=e[0]e.replace(p," "))).join(d.isWindows?"\r\n":"\n")}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(e){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._refresh()))),d.isLinux&&e&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:3===this._activeSelectionMode})}_isClickInSelection(e){const t=this._getMouseBufferCoords(e),i=this._model.finalSelectionStart,s=this._model.finalSelectionEnd;return!!(i&&s&&t)&&this._areCoordsInSelection(t,i,s)}isCellInSelection(e,t){const i=this._model.finalSelectionStart,s=this._model.finalSelectionEnd;return!(!i||!s)&&this._areCoordsInSelection([e,t],i,s)}_areCoordsInSelection(e,t,i){return e[1]>t[1]&&e[1]=t[0]&&e[0]=t[0]}_selectWordAtCursor(e,t){var i,s;const r=null===(s=null===(i=this._linkifier.currentLink)||void 0===i?void 0:i.link)||void 0===s?void 0:s.range;if(r)return this._model.selectionStart=[r.start.x-1,r.start.y-1],this._model.selectionStartLength=(0,_.getRangeLength)(r,this._bufferService.cols),this._model.selectionEnd=void 0,!0;const n=this._getMouseBufferCoords(e);return!!n&&(this._selectWordAt(n,t),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()}_handleTrim(e){this._model.handleTrim(e)&&this.refresh()}_getMouseBufferCoords(e){const t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t}_getMouseEventScrollAmount(e){let t=(0,n.getCoordsRelativeToElement)(this._coreBrowserService.window,e,this._screenElement)[1];const i=this._renderService.dimensions.css.canvas.height;return t>=0&&t<=i?0:(t>i&&(t-=i),t=Math.min(Math.max(t,-50),50),t/=50,t/Math.abs(t)+Math.round(14*t))}shouldForceSelection(e){return d.isMac?e.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:e.shiftKey}handleMouseDown(e){if(this._mouseDownTimeStamp=e.timeStamp,(2!==e.button||!this.hasSelection)&&0===e.button){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._handleIncrementalClick(e):1===e.detail?this._handleSingleClick(e):2===e.detail?this._handleDoubleClick(e):3===e.detail&&this._handleTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval((()=>this._dragScroll()),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))}_handleSingleClick(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),!this._model.selectionStart)return;this._model.selectionEnd=void 0;const t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&0===t.hasWidth(this._model.selectionStart[0])&&this._model.selectionStart[0]++}_handleDoubleClick(e){this._selectWordAtCursor(e,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(e){const t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))}shouldColumnSelect(e){return e.altKey&&!(d.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(e){if(e.stopImmediatePropagation(),!this._model.selectionStart)return;const t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),!this._model.selectionEnd)return void this.refresh(!0);2===this._activeSelectionMode?this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));const i=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}}_handleMouseUp(e){const t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&t<500&&e.altKey&&this._optionsService.rawOptions.altClickMovesCursor){if(this._bufferService.buffer.ybase===this._bufferService.buffer.ydisp){const t=this._mouseService.getCoords(e,this._element,this._bufferService.cols,this._bufferService.rows,!1);if(t&&void 0!==t[0]&&void 0!==t[1]){const e=(0,o.moveToCellSequence)(t[0]-1,t[1]-1,this._bufferService,this._coreService.decPrivateModes.applicationCursorKeys);this._coreService.triggerDataEvent(e,!0)}}}else this._fireEventIfSelectionChanged()}_fireEventIfSelectionChanged(){const e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd,i=!(!e||!t||e[0]===t[0]&&e[1]===t[1]);i?e&&t&&(this._oldSelectionStart&&this._oldSelectionEnd&&e[0]===this._oldSelectionStart[0]&&e[1]===this._oldSelectionStart[1]&&t[0]===this._oldSelectionEnd[0]&&t[1]===this._oldSelectionEnd[1]||this._fireOnSelectionChange(e,t,i)):this._oldHasSelection&&this._fireOnSelectionChange(e,t,i)}_fireOnSelectionChange(e,t,i){this._oldSelectionStart=e,this._oldSelectionEnd=t,this._oldHasSelection=i,this._onSelectionChange.fire()}_handleBufferActivate(e){this.clearSelection(),this._trimListener.dispose(),this._trimListener=e.activeBuffer.lines.onTrim((e=>this._handleTrim(e)))}_convertViewportColToCharacterIndex(e,t){let i=t;for(let s=0;t>=s;s++){const r=e.loadCell(s,this._workCell).getChars().length;0===this._workCell.getWidth()?i--:r>1&&t!==s&&(i+=r-1)}return i}setSelection(e,t,i){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=i,this.refresh(),this._fireEventIfSelectionChanged()}rightClickSelect(e){this._isClickInSelection(e)||(this._selectWordAtCursor(e,!1)&&this.refresh(!0),this._fireEventIfSelectionChanged())}_getWordAt(e,t,i=!0,s=!0){if(e[0]>=this._bufferService.cols)return;const r=this._bufferService.buffer,n=r.lines.get(e[1]);if(!n)return;const o=r.translateBufferLineToString(e[1],!1);let a=this._convertViewportColToCharacterIndex(n,e[0]),h=a;const c=e[0]-a;let l=0,d=0,_=0,u=0;if(" "===o.charAt(a)){for(;a>0&&" "===o.charAt(a-1);)a--;for(;h1&&(u+=s-1,h+=s-1);t>0&&a>0&&!this._isCharWordSeparator(n.loadCell(t-1,this._workCell));){n.loadCell(t-1,this._workCell);const e=this._workCell.getChars().length;0===this._workCell.getWidth()?(l++,t--):e>1&&(_+=e-1,a-=e-1),a--,t--}for(;i1&&(u+=e-1,h+=e-1),h++,i++}}h++;let f=a+c-l+_,v=Math.min(this._bufferService.cols,h-a+l+d-_-u);if(t||""!==o.slice(a,h).trim()){if(i&&0===f&&32!==n.getCodePoint(0)){const t=r.lines.get(e[1]-1);if(t&&n.isWrapped&&32!==t.getCodePoint(this._bufferService.cols-1)){const t=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(t){const e=this._bufferService.cols-t.start;f-=e,v+=e}}}if(s&&f+v===this._bufferService.cols&&32!==n.getCodePoint(this._bufferService.cols-1)){const t=r.lines.get(e[1]+1);if((null==t?void 0:t.isWrapped)&&32!==t.getCodePoint(0)){const t=this._getWordAt([0,e[1]+1],!1,!1,!0);t&&(v+=t.length)}}return{start:f,length:v}}}_selectWordAt(e,t){const i=this._getWordAt(e,t);if(i){for(;i.start<0;)i.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[i.start,e[1]],this._model.selectionStartLength=i.length}}_selectToWordAt(e){const t=this._getWordAt(e,!0);if(t){let i=e[1];for(;t.start<0;)t.start+=this._bufferService.cols,i--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,i++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,i]}}_isCharWordSeparator(e){return 0!==e.getWidth()&&this._optionsService.rawOptions.wordSeparator.indexOf(e.getChars())>=0}_selectLineAt(e){const t=this._bufferService.buffer.getWrappedRangeForLine(e),i={start:{x:0,y:t.first},end:{x:this._bufferService.cols-1,y:t.last}};this._model.selectionStart=[0,t.first],this._model.selectionEnd=void 0,this._model.selectionStartLength=(0,_.getRangeLength)(i,this._bufferService.cols)}};t.SelectionService=g=s([r(3,f.IBufferService),r(4,f.ICoreService),r(5,h.IMouseService),r(6,f.IOptionsService),r(7,h.IRenderService),r(8,h.ICoreBrowserService)],g)},4725:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.IThemeService=t.ICharacterJoinerService=t.ISelectionService=t.IRenderService=t.IMouseService=t.ICoreBrowserService=t.ICharSizeService=void 0;const s=i(8343);t.ICharSizeService=(0,s.createDecorator)("CharSizeService"),t.ICoreBrowserService=(0,s.createDecorator)("CoreBrowserService"),t.IMouseService=(0,s.createDecorator)("MouseService"),t.IRenderService=(0,s.createDecorator)("RenderService"),t.ISelectionService=(0,s.createDecorator)("SelectionService"),t.ICharacterJoinerService=(0,s.createDecorator)("CharacterJoinerService"),t.IThemeService=(0,s.createDecorator)("ThemeService")},6731:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.ThemeService=t.DEFAULT_ANSI_COLORS=void 0;const n=i(7239),o=i(8055),a=i(8460),h=i(844),c=i(2585),l=o.css.toColor("#ffffff"),d=o.css.toColor("#000000"),_=o.css.toColor("#ffffff"),u=o.css.toColor("#000000"),f={css:"rgba(255, 255, 255, 0.3)",rgba:4294967117};t.DEFAULT_ANSI_COLORS=Object.freeze((()=>{const e=[o.css.toColor("#2e3436"),o.css.toColor("#cc0000"),o.css.toColor("#4e9a06"),o.css.toColor("#c4a000"),o.css.toColor("#3465a4"),o.css.toColor("#75507b"),o.css.toColor("#06989a"),o.css.toColor("#d3d7cf"),o.css.toColor("#555753"),o.css.toColor("#ef2929"),o.css.toColor("#8ae234"),o.css.toColor("#fce94f"),o.css.toColor("#729fcf"),o.css.toColor("#ad7fa8"),o.css.toColor("#34e2e2"),o.css.toColor("#eeeeec")],t=[0,95,135,175,215,255];for(let i=0;i<216;i++){const s=t[i/36%6|0],r=t[i/6%6|0],n=t[i%6];e.push({css:o.channels.toCss(s,r,n),rgba:o.channels.toRgba(s,r,n)})}for(let t=0;t<24;t++){const i=8+10*t;e.push({css:o.channels.toCss(i,i,i),rgba:o.channels.toRgba(i,i,i)})}return e})());let v=t.ThemeService=class extends h.Disposable{get colors(){return this._colors}constructor(e){super(),this._optionsService=e,this._contrastCache=new n.ColorContrastCache,this._halfContrastCache=new n.ColorContrastCache,this._onChangeColors=this.register(new a.EventEmitter),this.onChangeColors=this._onChangeColors.event,this._colors={foreground:l,background:d,cursor:_,cursorAccent:u,selectionForeground:void 0,selectionBackgroundTransparent:f,selectionBackgroundOpaque:o.color.blend(d,f),selectionInactiveBackgroundTransparent:f,selectionInactiveBackgroundOpaque:o.color.blend(d,f),ansi:t.DEFAULT_ANSI_COLORS.slice(),contrastCache:this._contrastCache,halfContrastCache:this._halfContrastCache},this._updateRestoreColors(),this._setTheme(this._optionsService.rawOptions.theme),this.register(this._optionsService.onSpecificOptionChange("minimumContrastRatio",(()=>this._contrastCache.clear()))),this.register(this._optionsService.onSpecificOptionChange("theme",(()=>this._setTheme(this._optionsService.rawOptions.theme))))}_setTheme(e={}){const i=this._colors;if(i.foreground=p(e.foreground,l),i.background=p(e.background,d),i.cursor=p(e.cursor,_),i.cursorAccent=p(e.cursorAccent,u),i.selectionBackgroundTransparent=p(e.selectionBackground,f),i.selectionBackgroundOpaque=o.color.blend(i.background,i.selectionBackgroundTransparent),i.selectionInactiveBackgroundTransparent=p(e.selectionInactiveBackground,i.selectionBackgroundTransparent),i.selectionInactiveBackgroundOpaque=o.color.blend(i.background,i.selectionInactiveBackgroundTransparent),i.selectionForeground=e.selectionForeground?p(e.selectionForeground,o.NULL_COLOR):void 0,i.selectionForeground===o.NULL_COLOR&&(i.selectionForeground=void 0),o.color.isOpaque(i.selectionBackgroundTransparent)){const e=.3;i.selectionBackgroundTransparent=o.color.opacity(i.selectionBackgroundTransparent,e)}if(o.color.isOpaque(i.selectionInactiveBackgroundTransparent)){const e=.3;i.selectionInactiveBackgroundTransparent=o.color.opacity(i.selectionInactiveBackgroundTransparent,e)}if(i.ansi=t.DEFAULT_ANSI_COLORS.slice(),i.ansi[0]=p(e.black,t.DEFAULT_ANSI_COLORS[0]),i.ansi[1]=p(e.red,t.DEFAULT_ANSI_COLORS[1]),i.ansi[2]=p(e.green,t.DEFAULT_ANSI_COLORS[2]),i.ansi[3]=p(e.yellow,t.DEFAULT_ANSI_COLORS[3]),i.ansi[4]=p(e.blue,t.DEFAULT_ANSI_COLORS[4]),i.ansi[5]=p(e.magenta,t.DEFAULT_ANSI_COLORS[5]),i.ansi[6]=p(e.cyan,t.DEFAULT_ANSI_COLORS[6]),i.ansi[7]=p(e.white,t.DEFAULT_ANSI_COLORS[7]),i.ansi[8]=p(e.brightBlack,t.DEFAULT_ANSI_COLORS[8]),i.ansi[9]=p(e.brightRed,t.DEFAULT_ANSI_COLORS[9]),i.ansi[10]=p(e.brightGreen,t.DEFAULT_ANSI_COLORS[10]),i.ansi[11]=p(e.brightYellow,t.DEFAULT_ANSI_COLORS[11]),i.ansi[12]=p(e.brightBlue,t.DEFAULT_ANSI_COLORS[12]),i.ansi[13]=p(e.brightMagenta,t.DEFAULT_ANSI_COLORS[13]),i.ansi[14]=p(e.brightCyan,t.DEFAULT_ANSI_COLORS[14]),i.ansi[15]=p(e.brightWhite,t.DEFAULT_ANSI_COLORS[15]),e.extendedAnsi){const s=Math.min(i.ansi.length-16,e.extendedAnsi.length);for(let r=0;r{Object.defineProperty(t,"__esModule",{value:!0}),t.CircularList=void 0;const s=i(8460),r=i(844);class n extends r.Disposable{constructor(e){super(),this._maxLength=e,this.onDeleteEmitter=this.register(new s.EventEmitter),this.onDelete=this.onDeleteEmitter.event,this.onInsertEmitter=this.register(new s.EventEmitter),this.onInsert=this.onInsertEmitter.event,this.onTrimEmitter=this.register(new s.EventEmitter),this.onTrim=this.onTrimEmitter.event,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}get maxLength(){return this._maxLength}set maxLength(e){if(this._maxLength===e)return;const t=new Array(e);for(let i=0;ithis._length)for(let t=this._length;t=e;t--)this._array[this._getCyclicIndex(t+i.length)]=this._array[this._getCyclicIndex(t)];for(let t=0;tthis._maxLength){const e=this._length+i.length-this._maxLength;this._startIndex+=e,this._length=this._maxLength,this.onTrimEmitter.fire(e)}else this._length+=i.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,i){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+i<0)throw new Error("Cannot shift elements in list beyond index 0");if(i>0){for(let s=t-1;s>=0;s--)this.set(e+s+i,this.get(e+s));const s=e+t+i-this._length;if(s>0)for(this._length+=s;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let s=0;s{Object.defineProperty(t,"__esModule",{value:!0}),t.clone=void 0,t.clone=function e(t,i=5){if("object"!=typeof t)return t;const s=Array.isArray(t)?[]:{};for(const r in t)s[r]=i<=1?t[r]:t[r]&&e(t[r],i-1);return s}},8055:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.contrastRatio=t.toPaddedHex=t.rgba=t.rgb=t.css=t.color=t.channels=t.NULL_COLOR=void 0;const s=i(6114);let r=0,n=0,o=0,a=0;var h,c,l,d,_;function u(e){const t=e.toString(16);return t.length<2?"0"+t:t}function f(e,t){return e>>0}}(h||(t.channels=h={})),function(e){function t(e,t){return a=Math.round(255*t),[r,n,o]=_.toChannels(e.rgba),{css:h.toCss(r,n,o,a),rgba:h.toRgba(r,n,o,a)}}e.blend=function(e,t){if(a=(255&t.rgba)/255,1===a)return{css:t.css,rgba:t.rgba};const i=t.rgba>>24&255,s=t.rgba>>16&255,c=t.rgba>>8&255,l=e.rgba>>24&255,d=e.rgba>>16&255,_=e.rgba>>8&255;return r=l+Math.round((i-l)*a),n=d+Math.round((s-d)*a),o=_+Math.round((c-_)*a),{css:h.toCss(r,n,o),rgba:h.toRgba(r,n,o)}},e.isOpaque=function(e){return 255==(255&e.rgba)},e.ensureContrastRatio=function(e,t,i){const s=_.ensureContrastRatio(e.rgba,t.rgba,i);if(s)return _.toColor(s>>24&255,s>>16&255,s>>8&255)},e.opaque=function(e){const t=(255|e.rgba)>>>0;return[r,n,o]=_.toChannels(t),{css:h.toCss(r,n,o),rgba:t}},e.opacity=t,e.multiplyOpacity=function(e,i){return a=255&e.rgba,t(e,a*i/255)},e.toColorRGB=function(e){return[e.rgba>>24&255,e.rgba>>16&255,e.rgba>>8&255]}}(c||(t.color=c={})),function(e){let t,i;if(!s.isNode){const e=document.createElement("canvas");e.width=1,e.height=1;const s=e.getContext("2d",{willReadFrequently:!0});s&&(t=s,t.globalCompositeOperation="copy",i=t.createLinearGradient(0,0,1,1))}e.toColor=function(e){if(e.match(/#[\da-f]{3,8}/i))switch(e.length){case 4:return r=parseInt(e.slice(1,2).repeat(2),16),n=parseInt(e.slice(2,3).repeat(2),16),o=parseInt(e.slice(3,4).repeat(2),16),_.toColor(r,n,o);case 5:return r=parseInt(e.slice(1,2).repeat(2),16),n=parseInt(e.slice(2,3).repeat(2),16),o=parseInt(e.slice(3,4).repeat(2),16),a=parseInt(e.slice(4,5).repeat(2),16),_.toColor(r,n,o,a);case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}const s=e.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);if(s)return r=parseInt(s[1]),n=parseInt(s[2]),o=parseInt(s[3]),a=Math.round(255*(void 0===s[5]?1:parseFloat(s[5]))),_.toColor(r,n,o,a);if(!t||!i)throw new Error("css.toColor: Unsupported css format");if(t.fillStyle=i,t.fillStyle=e,"string"!=typeof t.fillStyle)throw new Error("css.toColor: Unsupported css format");if(t.fillRect(0,0,1,1),[r,n,o,a]=t.getImageData(0,0,1,1).data,255!==a)throw new Error("css.toColor: Unsupported css format");return{rgba:h.toRgba(r,n,o,a),css:e}}}(l||(t.css=l={})),function(e){function t(e,t,i){const s=e/255,r=t/255,n=i/255;return.2126*(s<=.03928?s/12.92:Math.pow((s+.055)/1.055,2.4))+.7152*(r<=.03928?r/12.92:Math.pow((r+.055)/1.055,2.4))+.0722*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))}e.relativeLuminance=function(e){return t(e>>16&255,e>>8&255,255&e)},e.relativeLuminance2=t}(d||(t.rgb=d={})),function(e){function t(e,t,i){const s=e>>24&255,r=e>>16&255,n=e>>8&255;let o=t>>24&255,a=t>>16&255,h=t>>8&255,c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));for(;c0||a>0||h>0);)o-=Math.max(0,Math.ceil(.1*o)),a-=Math.max(0,Math.ceil(.1*a)),h-=Math.max(0,Math.ceil(.1*h)),c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));return(o<<24|a<<16|h<<8|255)>>>0}function i(e,t,i){const s=e>>24&255,r=e>>16&255,n=e>>8&255;let o=t>>24&255,a=t>>16&255,h=t>>8&255,c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));for(;c>>0}e.ensureContrastRatio=function(e,s,r){const n=d.relativeLuminance(e>>8),o=d.relativeLuminance(s>>8);if(f(n,o)>8));if(af(n,d.relativeLuminance(t>>8))?o:t}return o}const a=i(e,s,r),h=f(n,d.relativeLuminance(a>>8));if(hf(n,d.relativeLuminance(i>>8))?a:i}return a}},e.reduceLuminance=t,e.increaseLuminance=i,e.toChannels=function(e){return[e>>24&255,e>>16&255,e>>8&255,255&e]},e.toColor=function(e,t,i,s){return{css:h.toCss(e,t,i,s),rgba:h.toRgba(e,t,i,s)}}}(_||(t.rgba=_={})),t.toPaddedHex=u,t.contrastRatio=f},8969:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.CoreTerminal=void 0;const s=i(844),r=i(2585),n=i(4348),o=i(7866),a=i(744),h=i(7302),c=i(6975),l=i(8460),d=i(1753),_=i(1480),u=i(7994),f=i(9282),v=i(5435),p=i(5981),g=i(2660);let m=!1;class S extends s.Disposable{get onScroll(){return this._onScrollApi||(this._onScrollApi=this.register(new l.EventEmitter),this._onScroll.event((e=>{var t;null===(t=this._onScrollApi)||void 0===t||t.fire(e.position)}))),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(const t in e)this.optionsService.options[t]=e[t]}constructor(e){super(),this._windowsWrappingHeuristics=this.register(new s.MutableDisposable),this._onBinary=this.register(new l.EventEmitter),this.onBinary=this._onBinary.event,this._onData=this.register(new l.EventEmitter),this.onData=this._onData.event,this._onLineFeed=this.register(new l.EventEmitter),this.onLineFeed=this._onLineFeed.event,this._onResize=this.register(new l.EventEmitter),this.onResize=this._onResize.event,this._onWriteParsed=this.register(new l.EventEmitter),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this.register(new l.EventEmitter),this._instantiationService=new n.InstantiationService,this.optionsService=this.register(new h.OptionsService(e)),this._instantiationService.setService(r.IOptionsService,this.optionsService),this._bufferService=this.register(this._instantiationService.createInstance(a.BufferService)),this._instantiationService.setService(r.IBufferService,this._bufferService),this._logService=this.register(this._instantiationService.createInstance(o.LogService)),this._instantiationService.setService(r.ILogService,this._logService),this.coreService=this.register(this._instantiationService.createInstance(c.CoreService)),this._instantiationService.setService(r.ICoreService,this.coreService),this.coreMouseService=this.register(this._instantiationService.createInstance(d.CoreMouseService)),this._instantiationService.setService(r.ICoreMouseService,this.coreMouseService),this.unicodeService=this.register(this._instantiationService.createInstance(_.UnicodeService)),this._instantiationService.setService(r.IUnicodeService,this.unicodeService),this._charsetService=this._instantiationService.createInstance(u.CharsetService),this._instantiationService.setService(r.ICharsetService,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(g.OscLinkService),this._instantiationService.setService(r.IOscLinkService,this._oscLinkService),this._inputHandler=this.register(new v.InputHandler(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.coreMouseService,this.unicodeService)),this.register((0,l.forwardEvent)(this._inputHandler.onLineFeed,this._onLineFeed)),this.register(this._inputHandler),this.register((0,l.forwardEvent)(this._bufferService.onResize,this._onResize)),this.register((0,l.forwardEvent)(this.coreService.onData,this._onData)),this.register((0,l.forwardEvent)(this.coreService.onBinary,this._onBinary)),this.register(this.coreService.onRequestScrollToBottom((()=>this.scrollToBottom()))),this.register(this.coreService.onUserInput((()=>this._writeBuffer.handleUserInput()))),this.register(this.optionsService.onMultipleOptionChange(["windowsMode","windowsPty"],(()=>this._handleWindowsPtyOptionChange()))),this.register(this._bufferService.onScroll((e=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp,source:0}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)}))),this.register(this._inputHandler.onScroll((e=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp,source:0}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)}))),this._writeBuffer=this.register(new p.WriteBuffer(((e,t)=>this._inputHandler.parse(e,t)))),this.register((0,l.forwardEvent)(this._writeBuffer.onWriteParsed,this._onWriteParsed))}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=r.LogLevelEnum.WARN&&!m&&(this._logService.warn("writeSync is unreliable and will be removed soon."),m=!0),this._writeBuffer.writeSync(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,a.MINIMUM_COLS),t=Math.max(t,a.MINIMUM_ROWS),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t,i){this._bufferService.scrollLines(e,t,i)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){const t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.coreMouseService.reset()}_handleWindowsPtyOptionChange(){let e=!1;const t=this.optionsService.rawOptions.windowsPty;t&&void 0!==t.buildNumber&&void 0!==t.buildNumber?e=!!("conpty"===t.backend&&t.buildNumber<21376):this.optionsService.rawOptions.windowsMode&&(e=!0),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){const e=[];e.push(this.onLineFeed(f.updateWindowsModeWrappedState.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:"H"},(()=>((0,f.updateWindowsModeWrappedState)(this._bufferService),!1)))),this._windowsWrappingHeuristics.value=(0,s.toDisposable)((()=>{for(const t of e)t.dispose()}))}}}t.CoreTerminal=S},8460:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.forwardEvent=t.EventEmitter=void 0,t.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=e=>(this._listeners.push(e),{dispose:()=>{if(!this._disposed)for(let t=0;tt.fire(e)))}},5435:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.InputHandler=t.WindowsOptionsReportType=void 0;const n=i(2584),o=i(7116),a=i(2015),h=i(844),c=i(482),l=i(8437),d=i(8460),_=i(643),u=i(511),f=i(3734),v=i(2585),p=i(6242),g=i(6351),m=i(5941),S={"(":0,")":1,"*":2,"+":3,"-":1,".":2},C=131072;function b(e,t){if(e>24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}var y;!function(e){e[e.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",e[e.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"}(y||(t.WindowsOptionsReportType=y={}));let w=0;class E extends h.Disposable{getAttrData(){return this._curAttrData}constructor(e,t,i,s,r,h,_,f,v=new a.EscapeSequenceParser){super(),this._bufferService=e,this._charsetService=t,this._coreService=i,this._logService=s,this._optionsService=r,this._oscLinkService=h,this._coreMouseService=_,this._unicodeService=f,this._parser=v,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new c.StringToUtf32,this._utf8Decoder=new c.Utf8ToUtf32,this._workCell=new u.CellData,this._windowTitle="",this._iconName="",this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=l.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=l.DEFAULT_ATTR_DATA.clone(),this._onRequestBell=this.register(new d.EventEmitter),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this.register(new d.EventEmitter),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this.register(new d.EventEmitter),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this.register(new d.EventEmitter),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this.register(new d.EventEmitter),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this.register(new d.EventEmitter),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this.register(new d.EventEmitter),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this.register(new d.EventEmitter),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this.register(new d.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this.register(new d.EventEmitter),this.onLineFeed=this._onLineFeed.event,this._onScroll=this.register(new d.EventEmitter),this.onScroll=this._onScroll.event,this._onTitleChange=this.register(new d.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onColor=this.register(new d.EventEmitter),this.onColor=this._onColor.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this.register(this._parser),this._dirtyRowTracker=new k(this._bufferService),this._activeBuffer=this._bufferService.buffer,this.register(this._bufferService.buffers.onBufferActivate((e=>this._activeBuffer=e.activeBuffer))),this._parser.setCsiHandlerFallback(((e,t)=>{this._logService.debug("Unknown CSI code: ",{identifier:this._parser.identToString(e),params:t.toArray()})})),this._parser.setEscHandlerFallback((e=>{this._logService.debug("Unknown ESC code: ",{identifier:this._parser.identToString(e)})})),this._parser.setExecuteHandlerFallback((e=>{this._logService.debug("Unknown EXECUTE code: ",{code:e})})),this._parser.setOscHandlerFallback(((e,t,i)=>{this._logService.debug("Unknown OSC code: ",{identifier:e,action:t,data:i})})),this._parser.setDcsHandlerFallback(((e,t,i)=>{"HOOK"===t&&(i=i.toArray()),this._logService.debug("Unknown DCS code: ",{identifier:this._parser.identToString(e),action:t,payload:i})})),this._parser.setPrintHandler(((e,t,i)=>this.print(e,t,i))),this._parser.registerCsiHandler({final:"@"},(e=>this.insertChars(e))),this._parser.registerCsiHandler({intermediates:" ",final:"@"},(e=>this.scrollLeft(e))),this._parser.registerCsiHandler({final:"A"},(e=>this.cursorUp(e))),this._parser.registerCsiHandler({intermediates:" ",final:"A"},(e=>this.scrollRight(e))),this._parser.registerCsiHandler({final:"B"},(e=>this.cursorDown(e))),this._parser.registerCsiHandler({final:"C"},(e=>this.cursorForward(e))),this._parser.registerCsiHandler({final:"D"},(e=>this.cursorBackward(e))),this._parser.registerCsiHandler({final:"E"},(e=>this.cursorNextLine(e))),this._parser.registerCsiHandler({final:"F"},(e=>this.cursorPrecedingLine(e))),this._parser.registerCsiHandler({final:"G"},(e=>this.cursorCharAbsolute(e))),this._parser.registerCsiHandler({final:"H"},(e=>this.cursorPosition(e))),this._parser.registerCsiHandler({final:"I"},(e=>this.cursorForwardTab(e))),this._parser.registerCsiHandler({final:"J"},(e=>this.eraseInDisplay(e,!1))),this._parser.registerCsiHandler({prefix:"?",final:"J"},(e=>this.eraseInDisplay(e,!0))),this._parser.registerCsiHandler({final:"K"},(e=>this.eraseInLine(e,!1))),this._parser.registerCsiHandler({prefix:"?",final:"K"},(e=>this.eraseInLine(e,!0))),this._parser.registerCsiHandler({final:"L"},(e=>this.insertLines(e))),this._parser.registerCsiHandler({final:"M"},(e=>this.deleteLines(e))),this._parser.registerCsiHandler({final:"P"},(e=>this.deleteChars(e))),this._parser.registerCsiHandler({final:"S"},(e=>this.scrollUp(e))),this._parser.registerCsiHandler({final:"T"},(e=>this.scrollDown(e))),this._parser.registerCsiHandler({final:"X"},(e=>this.eraseChars(e))),this._parser.registerCsiHandler({final:"Z"},(e=>this.cursorBackwardTab(e))),this._parser.registerCsiHandler({final:"`"},(e=>this.charPosAbsolute(e))),this._parser.registerCsiHandler({final:"a"},(e=>this.hPositionRelative(e))),this._parser.registerCsiHandler({final:"b"},(e=>this.repeatPrecedingCharacter(e))),this._parser.registerCsiHandler({final:"c"},(e=>this.sendDeviceAttributesPrimary(e))),this._parser.registerCsiHandler({prefix:">",final:"c"},(e=>this.sendDeviceAttributesSecondary(e))),this._parser.registerCsiHandler({final:"d"},(e=>this.linePosAbsolute(e))),this._parser.registerCsiHandler({final:"e"},(e=>this.vPositionRelative(e))),this._parser.registerCsiHandler({final:"f"},(e=>this.hVPosition(e))),this._parser.registerCsiHandler({final:"g"},(e=>this.tabClear(e))),this._parser.registerCsiHandler({final:"h"},(e=>this.setMode(e))),this._parser.registerCsiHandler({prefix:"?",final:"h"},(e=>this.setModePrivate(e))),this._parser.registerCsiHandler({final:"l"},(e=>this.resetMode(e))),this._parser.registerCsiHandler({prefix:"?",final:"l"},(e=>this.resetModePrivate(e))),this._parser.registerCsiHandler({final:"m"},(e=>this.charAttributes(e))),this._parser.registerCsiHandler({final:"n"},(e=>this.deviceStatus(e))),this._parser.registerCsiHandler({prefix:"?",final:"n"},(e=>this.deviceStatusPrivate(e))),this._parser.registerCsiHandler({intermediates:"!",final:"p"},(e=>this.softReset(e))),this._parser.registerCsiHandler({intermediates:" ",final:"q"},(e=>this.setCursorStyle(e))),this._parser.registerCsiHandler({final:"r"},(e=>this.setScrollRegion(e))),this._parser.registerCsiHandler({final:"s"},(e=>this.saveCursor(e))),this._parser.registerCsiHandler({final:"t"},(e=>this.windowOptions(e))),this._parser.registerCsiHandler({final:"u"},(e=>this.restoreCursor(e))),this._parser.registerCsiHandler({intermediates:"'",final:"}"},(e=>this.insertColumns(e))),this._parser.registerCsiHandler({intermediates:"'",final:"~"},(e=>this.deleteColumns(e))),this._parser.registerCsiHandler({intermediates:'"',final:"q"},(e=>this.selectProtected(e))),this._parser.registerCsiHandler({intermediates:"$",final:"p"},(e=>this.requestMode(e,!0))),this._parser.registerCsiHandler({prefix:"?",intermediates:"$",final:"p"},(e=>this.requestMode(e,!1))),this._parser.setExecuteHandler(n.C0.BEL,(()=>this.bell())),this._parser.setExecuteHandler(n.C0.LF,(()=>this.lineFeed())),this._parser.setExecuteHandler(n.C0.VT,(()=>this.lineFeed())),this._parser.setExecuteHandler(n.C0.FF,(()=>this.lineFeed())),this._parser.setExecuteHandler(n.C0.CR,(()=>this.carriageReturn())),this._parser.setExecuteHandler(n.C0.BS,(()=>this.backspace())),this._parser.setExecuteHandler(n.C0.HT,(()=>this.tab())),this._parser.setExecuteHandler(n.C0.SO,(()=>this.shiftOut())),this._parser.setExecuteHandler(n.C0.SI,(()=>this.shiftIn())),this._parser.setExecuteHandler(n.C1.IND,(()=>this.index())),this._parser.setExecuteHandler(n.C1.NEL,(()=>this.nextLine())),this._parser.setExecuteHandler(n.C1.HTS,(()=>this.tabSet())),this._parser.registerOscHandler(0,new p.OscHandler((e=>(this.setTitle(e),this.setIconName(e),!0)))),this._parser.registerOscHandler(1,new p.OscHandler((e=>this.setIconName(e)))),this._parser.registerOscHandler(2,new p.OscHandler((e=>this.setTitle(e)))),this._parser.registerOscHandler(4,new p.OscHandler((e=>this.setOrReportIndexedColor(e)))),this._parser.registerOscHandler(8,new p.OscHandler((e=>this.setHyperlink(e)))),this._parser.registerOscHandler(10,new p.OscHandler((e=>this.setOrReportFgColor(e)))),this._parser.registerOscHandler(11,new p.OscHandler((e=>this.setOrReportBgColor(e)))),this._parser.registerOscHandler(12,new p.OscHandler((e=>this.setOrReportCursorColor(e)))),this._parser.registerOscHandler(104,new p.OscHandler((e=>this.restoreIndexedColor(e)))),this._parser.registerOscHandler(110,new p.OscHandler((e=>this.restoreFgColor(e)))),this._parser.registerOscHandler(111,new p.OscHandler((e=>this.restoreBgColor(e)))),this._parser.registerOscHandler(112,new p.OscHandler((e=>this.restoreCursorColor(e)))),this._parser.registerEscHandler({final:"7"},(()=>this.saveCursor())),this._parser.registerEscHandler({final:"8"},(()=>this.restoreCursor())),this._parser.registerEscHandler({final:"D"},(()=>this.index())),this._parser.registerEscHandler({final:"E"},(()=>this.nextLine())),this._parser.registerEscHandler({final:"H"},(()=>this.tabSet())),this._parser.registerEscHandler({final:"M"},(()=>this.reverseIndex())),this._parser.registerEscHandler({final:"="},(()=>this.keypadApplicationMode())),this._parser.registerEscHandler({final:">"},(()=>this.keypadNumericMode())),this._parser.registerEscHandler({final:"c"},(()=>this.fullReset())),this._parser.registerEscHandler({final:"n"},(()=>this.setgLevel(2))),this._parser.registerEscHandler({final:"o"},(()=>this.setgLevel(3))),this._parser.registerEscHandler({final:"|"},(()=>this.setgLevel(3))),this._parser.registerEscHandler({final:"}"},(()=>this.setgLevel(2))),this._parser.registerEscHandler({final:"~"},(()=>this.setgLevel(1))),this._parser.registerEscHandler({intermediates:"%",final:"@"},(()=>this.selectDefaultCharset())),this._parser.registerEscHandler({intermediates:"%",final:"G"},(()=>this.selectDefaultCharset()));for(const e in o.CHARSETS)this._parser.registerEscHandler({intermediates:"(",final:e},(()=>this.selectCharset("("+e))),this._parser.registerEscHandler({intermediates:")",final:e},(()=>this.selectCharset(")"+e))),this._parser.registerEscHandler({intermediates:"*",final:e},(()=>this.selectCharset("*"+e))),this._parser.registerEscHandler({intermediates:"+",final:e},(()=>this.selectCharset("+"+e))),this._parser.registerEscHandler({intermediates:"-",final:e},(()=>this.selectCharset("-"+e))),this._parser.registerEscHandler({intermediates:".",final:e},(()=>this.selectCharset("."+e))),this._parser.registerEscHandler({intermediates:"/",final:e},(()=>this.selectCharset("/"+e)));this._parser.registerEscHandler({intermediates:"#",final:"8"},(()=>this.screenAlignmentPattern())),this._parser.setErrorHandler((e=>(this._logService.error("Parsing error: ",e),e))),this._parser.registerDcsHandler({intermediates:"$",final:"q"},new g.DcsHandler(((e,t)=>this.requestStatusString(e,t))))}_preserveStack(e,t,i,s){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=i,this._parseStack.position=s}_logSlowResolvingAsync(e){this._logService.logLevel<=v.LogLevelEnum.WARN&&Promise.race([e,new Promise(((e,t)=>setTimeout((()=>t("#SLOW_TIMEOUT")),5e3)))]).catch((e=>{if("#SLOW_TIMEOUT"!==e)throw e;console.warn("async parser handler taking longer than 5000 ms")}))}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let i,s=this._activeBuffer.x,r=this._activeBuffer.y,n=0;const o=this._parseStack.paused;if(o){if(i=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(i),i;s=this._parseStack.cursorStartX,r=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>C&&(n=this._parseStack.position+C)}if(this._logService.logLevel<=v.LogLevelEnum.DEBUG&&this._logService.debug("parsing data"+("string"==typeof e?` "${e}"`:` "${Array.prototype.map.call(e,(e=>String.fromCharCode(e))).join("")}"`),"string"==typeof e?e.split("").map((e=>e.charCodeAt(0))):e),this._parseBuffer.lengthC)for(let t=n;t0&&2===u.getWidth(this._activeBuffer.x-1)&&u.setCellFromCodePoint(this._activeBuffer.x-1,0,1,d.fg,d.bg,d.extended);for(let f=t;f=a)if(h){for(;this._activeBuffer.x=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),u=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)}else if(this._activeBuffer.x=a-1,2===r)continue;if(l&&(u.insertCells(this._activeBuffer.x,r,this._activeBuffer.getNullCell(d),d),2===u.getWidth(a-1)&&u.setCellFromCodePoint(a-1,_.NULL_CELL_CODE,_.NULL_CELL_WIDTH,d.fg,d.bg,d.extended)),u.setCellFromCodePoint(this._activeBuffer.x++,s,r,d.fg,d.bg,d.extended),r>0)for(;--r;)u.setCellFromCodePoint(this._activeBuffer.x++,0,0,d.fg,d.bg,d.extended)}else u.getWidth(this._activeBuffer.x-1)?u.addCodepointToCell(this._activeBuffer.x-1,s):u.addCodepointToCell(this._activeBuffer.x-2,s)}i-t>0&&(u.loadCell(this._activeBuffer.x-1,this._workCell),2===this._workCell.getWidth()||this._workCell.getCode()>65535?this._parser.precedingCodepoint=0:this._workCell.isCombined()?this._parser.precedingCodepoint=this._workCell.getChars().charCodeAt(0):this._parser.precedingCodepoint=this._workCell.content),this._activeBuffer.x0&&0===u.getWidth(this._activeBuffer.x)&&!u.hasContent(this._activeBuffer.x)&&u.setCellFromCodePoint(this._activeBuffer.x,0,1,d.fg,d.bg,d.extended),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return"t"!==e.final||e.prefix||e.intermediates?this._parser.registerCsiHandler(e,t):this._parser.registerCsiHandler(e,(e=>!b(e.params[0],this._optionsService.rawOptions.windowOptions)||t(e)))}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new g.DcsHandler(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new p.OscHandler(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){var e;if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(0===this._activeBuffer.x&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&(null===(e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y))||void 0===e?void 0:e.isWrapped)){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;const e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;const e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){const t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){const t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){const t=e.params[0];return 0===t?delete this._activeBuffer.tabs[this._activeBuffer.x]:3===t&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){const t=e.params[0];return 1===t&&(this._curAttrData.bg|=536870912),2!==t&&0!==t||(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,i,s=!1,r=!1){const n=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);n.replaceCells(t,i,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData(),r),s&&(n.isWrapped=!1)}_resetBufferLine(e,t=!1){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i&&(i.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),i.isWrapped=!1)}eraseInDisplay(e,t=!1){let i;switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:for(i=this._activeBuffer.y,this._dirtyRowTracker.markDirty(i),this._eraseInBufferLine(i++,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);i=this._bufferService.cols&&(this._activeBuffer.lines.get(i+1).isWrapped=!1);i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0);break;case 2:for(i=this._bufferService.rows,this._dirtyRowTracker.markDirty(i-1);i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0);break;case 3:const e=this._activeBuffer.lines.length-this._bufferService.rows;e>0&&(this._activeBuffer.lines.trimStart(e),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-e,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-e,0),this._onScroll.fire(0))}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t)}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent(n.C0.ESC+"[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent(n.C0.ESC+"[?6c")),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent(n.C0.ESC+"[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent(n.C0.ESC+"[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent(n.C0.ESC+"[>83;40003;0c")),!0}_is(e){return 0===(this._optionsService.rawOptions.termName+"").indexOf(e)}setMode(e){for(let t=0;te?1:2,u=e.params[0];return f=u,v=t?2===u?4:4===u?_(o.modes.insertMode):12===u?3:20===u?_(d.convertEol):0:1===u?_(i.applicationCursorKeys):3===u?d.windowOptions.setWinLines?80===h?2:132===h?1:0:0:6===u?_(i.origin):7===u?_(i.wraparound):8===u?3:9===u?_("X10"===s):12===u?_(d.cursorBlink):25===u?_(!o.isCursorHidden):45===u?_(i.reverseWraparound):66===u?_(i.applicationKeypad):67===u?4:1e3===u?_("VT200"===s):1002===u?_("DRAG"===s):1003===u?_("ANY"===s):1004===u?_(i.sendFocus):1005===u?4:1006===u?_("SGR"===r):1015===u?4:1016===u?_("SGR_PIXELS"===r):1048===u?1:47===u||1047===u||1049===u?_(c===l):2004===u?_(i.bracketedPasteMode):0,o.triggerDataEvent(`${n.C0.ESC}[${t?"":"?"}${f};${v}$y`),!0;var f,v}_updateAttrColor(e,t,i,s,r){return 2===t?(e|=50331648,e&=-16777216,e|=f.AttributeData.fromColorRGB([i,s,r])):5===t&&(e&=-50331904,e|=33554432|255&i),e}_extractColor(e,t,i){const s=[0,0,-1,0,0,0];let r=0,n=0;do{if(s[n+r]=e.params[t+n],e.hasSubParams(t+n)){const i=e.getSubParams(t+n);let o=0;do{5===s[1]&&(r=1),s[n+o+1+r]=i[o]}while(++o=2||2===s[1]&&n+r>=5)break;s[1]&&(r=1)}while(++n+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=l.DEFAULT_ATTR_DATA.fg,e.bg=l.DEFAULT_ATTR_DATA.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(1===e.length&&0===e.params[0])return this._processSGR0(this._curAttrData),!0;const t=e.length;let i;const s=this._curAttrData;for(let r=0;r=30&&i<=37?(s.fg&=-50331904,s.fg|=16777216|i-30):i>=40&&i<=47?(s.bg&=-50331904,s.bg|=16777216|i-40):i>=90&&i<=97?(s.fg&=-50331904,s.fg|=16777224|i-90):i>=100&&i<=107?(s.bg&=-50331904,s.bg|=16777224|i-100):0===i?this._processSGR0(s):1===i?s.fg|=134217728:3===i?s.bg|=67108864:4===i?(s.fg|=268435456,this._processUnderline(e.hasSubParams(r)?e.getSubParams(r)[0]:1,s)):5===i?s.fg|=536870912:7===i?s.fg|=67108864:8===i?s.fg|=1073741824:9===i?s.fg|=2147483648:2===i?s.bg|=134217728:21===i?this._processUnderline(2,s):22===i?(s.fg&=-134217729,s.bg&=-134217729):23===i?s.bg&=-67108865:24===i?(s.fg&=-268435457,this._processUnderline(0,s)):25===i?s.fg&=-536870913:27===i?s.fg&=-67108865:28===i?s.fg&=-1073741825:29===i?s.fg&=2147483647:39===i?(s.fg&=-67108864,s.fg|=16777215&l.DEFAULT_ATTR_DATA.fg):49===i?(s.bg&=-67108864,s.bg|=16777215&l.DEFAULT_ATTR_DATA.bg):38===i||48===i||58===i?r+=this._extractColor(e,r,s):53===i?s.bg|=1073741824:55===i?s.bg&=-1073741825:59===i?(s.extended=s.extended.clone(),s.extended.underlineColor=-1,s.updateExtended()):100===i?(s.fg&=-67108864,s.fg|=16777215&l.DEFAULT_ATTR_DATA.fg,s.bg&=-67108864,s.bg|=16777215&l.DEFAULT_ATTR_DATA.bg):this._logService.debug("Unknown SGR attribute: %d.",i);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(`${n.C0.ESC}[0n`);break;case 6:const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${n.C0.ESC}[${e};${t}R`)}return!0}deviceStatusPrivate(e){if(6===e.params[0]){const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${n.C0.ESC}[?${e};${t}R`)}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=l.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){const t=e.params[0]||1;switch(t){case 1:case 2:this._optionsService.options.cursorStyle="block";break;case 3:case 4:this._optionsService.options.cursorStyle="underline";break;case 5:case 6:this._optionsService.options.cursorStyle="bar"}const i=t%2==1;return this._optionsService.options.cursorBlink=i,!0}setScrollRegion(e){const t=e.params[0]||1;let i;return(e.length<2||(i=e.params[1])>this._bufferService.rows||0===i)&&(i=this._bufferService.rows),i>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=i-1,this._setCursor(0,0)),!0}windowOptions(e){if(!b(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;const t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(y.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(y.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`${n.C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,!0}restoreCursor(e){return this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._activeBuffer.savedCharset&&(this._charsetService.charset=this._activeBuffer.savedCharset),this._restrictCursor(),!0}setTitle(e){return this._windowTitle=e,this._onTitleChange.fire(e),!0}setIconName(e){return this._iconName=e,!0}setOrReportIndexedColor(e){const t=[],i=e.split(";");for(;i.length>1;){const e=i.shift(),s=i.shift();if(/^\d+$/.exec(e)){const i=parseInt(e);if(L(i))if("?"===s)t.push({type:0,index:i});else{const e=(0,m.parseColor)(s);e&&t.push({type:1,index:i,color:e})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){const t=e.split(";");return!(t.length<2)&&(t[1]?this._createHyperlink(t[0],t[1]):!t[0]&&this._finishHyperlink())}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();const i=e.split(":");let s;const r=i.findIndex((e=>e.startsWith("id=")));return-1!==r&&(s=i[r].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:s,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){const i=e.split(";");for(let e=0;e=this._specialColors.length);++e,++t)if("?"===i[e])this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{const s=(0,m.parseColor)(i[e]);s&&this._onColor.fire([{type:1,index:this._specialColors[t],color:s}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;const t=[],i=e.split(";");for(let e=0;e=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){const e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=l.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=l.DEFAULT_ATTR_DATA.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){const e=new u.CellData;e.content=1<<22|"E".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`${n.C0.ESC}${e}${n.C0.ESC}\\`),!0))('"q'===e?`P1$r${this._curAttrData.isProtected()?1:0}"q`:'"p'===e?'P1$r61;1"p':"r"===e?`P1$r${i.scrollTop+1};${i.scrollBottom+1}r`:"m"===e?"P1$r0m":" q"===e?`P1$r${{block:2,underline:4,bar:6}[s.cursorStyle]-(s.cursorBlink?1:0)} q`:"P0$r")}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}}t.InputHandler=E;let k=class{constructor(e){this._bufferService=e,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(e){ethis.end&&(this.end=e)}markRangeDirty(e,t){e>t&&(w=e,e=t,t=w),ethis.end&&(this.end=t)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};function L(e){return 0<=e&&e<256}k=s([r(0,v.IBufferService)],k)},844:(e,t)=>{function i(e){for(const t of e)t.dispose();e.length=0}Object.defineProperty(t,"__esModule",{value:!0}),t.getDisposeArrayDisposable=t.disposeArray=t.toDisposable=t.MutableDisposable=t.Disposable=void 0,t.Disposable=class{constructor(){this._disposables=[],this._isDisposed=!1}dispose(){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.length=0}register(e){return this._disposables.push(e),e}unregister(e){const t=this._disposables.indexOf(e);-1!==t&&this._disposables.splice(t,1)}},t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){var t;this._isDisposed||e===this._value||(null===(t=this._value)||void 0===t||t.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){var e;this._isDisposed=!0,null===(e=this._value)||void 0===e||e.dispose(),this._value=void 0}},t.toDisposable=function(e){return{dispose:e}},t.disposeArray=i,t.getDisposeArrayDisposable=function(e){return{dispose:()=>i(e)}}},1505:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.FourKeyMap=t.TwoKeyMap=void 0;class i{constructor(){this._data={}}set(e,t,i){this._data[e]||(this._data[e]={}),this._data[e][t]=i}get(e,t){return this._data[e]?this._data[e][t]:void 0}clear(){this._data={}}}t.TwoKeyMap=i,t.FourKeyMap=class{constructor(){this._data=new i}set(e,t,s,r,n){this._data.get(e,t)||this._data.set(e,t,new i),this._data.get(e,t).set(s,r,n)}get(e,t,i,s){var r;return null===(r=this._data.get(e,t))||void 0===r?void 0:r.get(i,s)}clear(){this._data.clear()}}},6114:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.isChromeOS=t.isLinux=t.isWindows=t.isIphone=t.isIpad=t.isMac=t.getSafariVersion=t.isSafari=t.isLegacyEdge=t.isFirefox=t.isNode=void 0,t.isNode="undefined"==typeof navigator;const i=t.isNode?"node":navigator.userAgent,s=t.isNode?"node":navigator.platform;t.isFirefox=i.includes("Firefox"),t.isLegacyEdge=i.includes("Edge"),t.isSafari=/^((?!chrome|android).)*safari/i.test(i),t.getSafariVersion=function(){if(!t.isSafari)return 0;const e=i.match(/Version\/(\d+)/);return null===e||e.length<2?0:parseInt(e[1])},t.isMac=["Macintosh","MacIntel","MacPPC","Mac68K"].includes(s),t.isIpad="iPad"===s,t.isIphone="iPhone"===s,t.isWindows=["Windows","Win16","Win32","WinCE"].includes(s),t.isLinux=s.indexOf("Linux")>=0,t.isChromeOS=/\bCrOS\b/.test(i)},6106:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.SortedList=void 0;let i=0;t.SortedList=class{constructor(e){this._getKey=e,this._array=[]}clear(){this._array.length=0}insert(e){0!==this._array.length?(i=this._search(this._getKey(e)),this._array.splice(i,0,e)):this._array.push(e)}delete(e){if(0===this._array.length)return!1;const t=this._getKey(e);if(void 0===t)return!1;if(i=this._search(t),-1===i)return!1;if(this._getKey(this._array[i])!==t)return!1;do{if(this._array[i]===e)return this._array.splice(i,1),!0}while(++i=this._array.length)&&this._getKey(this._array[i])===e))do{yield this._array[i]}while(++i=this._array.length)&&this._getKey(this._array[i])===e))do{t(this._array[i])}while(++i=t;){let s=t+i>>1;const r=this._getKey(this._array[s]);if(r>e)i=s-1;else{if(!(r0&&this._getKey(this._array[s-1])===e;)s--;return s}t=s+1}}return t}}},7226:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.DebouncedIdleTask=t.IdleTaskQueue=t.PriorityTaskQueue=void 0;const s=i(6114);class r{constructor(){this._tasks=[],this._i=0}enqueue(e){this._tasks.push(e),this._start()}flush(){for(;this._ir)return s-t<-20&&console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(s-t))}ms`),void this._start();s=r}this.clear()}}class n extends r{_requestCallback(e){return setTimeout((()=>e(this._createDeadline(16))))}_cancelCallback(e){clearTimeout(e)}_createDeadline(e){const t=Date.now()+e;return{timeRemaining:()=>Math.max(0,t-Date.now())}}}t.PriorityTaskQueue=n,t.IdleTaskQueue=!s.isNode&&"requestIdleCallback"in window?class extends r{_requestCallback(e){return requestIdleCallback(e)}_cancelCallback(e){cancelIdleCallback(e)}}:n,t.DebouncedIdleTask=class{constructor(){this._queue=new t.IdleTaskQueue}set(e){this._queue.clear(),this._queue.enqueue(e)}flush(){this._queue.flush()}}},9282:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.updateWindowsModeWrappedState=void 0;const s=i(643);t.updateWindowsModeWrappedState=function(e){const t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1),i=null==t?void 0:t.get(e.cols-1),r=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);r&&i&&(r.isWrapped=i[s.CHAR_DATA_CODE_INDEX]!==s.NULL_CELL_CODE&&i[s.CHAR_DATA_CODE_INDEX]!==s.WHITESPACE_CELL_CODE)}},3734:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ExtendedAttrs=t.AttributeData=void 0;class i{constructor(){this.fg=0,this.bg=0,this.extended=new s}static toColorRGB(e){return[e>>>16&255,e>>>8&255,255&e]}static fromColorRGB(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]}clone(){const e=new i;return e.fg=this.fg,e.bg=this.bg,e.extended=this.extended.clone(),e}isInverse(){return 67108864&this.fg}isBold(){return 134217728&this.fg}isUnderline(){return this.hasExtendedAttrs()&&0!==this.extended.underlineStyle?1:268435456&this.fg}isBlink(){return 536870912&this.fg}isInvisible(){return 1073741824&this.fg}isItalic(){return 67108864&this.bg}isDim(){return 134217728&this.bg}isStrikethrough(){return 2147483648&this.fg}isProtected(){return 536870912&this.bg}isOverline(){return 1073741824&this.bg}getFgColorMode(){return 50331648&this.fg}getBgColorMode(){return 50331648&this.bg}isFgRGB(){return 50331648==(50331648&this.fg)}isBgRGB(){return 50331648==(50331648&this.bg)}isFgPalette(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)}isBgPalette(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)}isFgDefault(){return 0==(50331648&this.fg)}isBgDefault(){return 0==(50331648&this.bg)}isAttributeDefault(){return 0===this.fg&&0===this.bg}getFgColor(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}}getBgColor(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}}hasExtendedAttrs(){return 268435456&this.bg}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()}isUnderlineColorRGB(){return 268435456&this.bg&&~this.extended.underlineColor?50331648==(50331648&this.extended.underlineColor):this.isFgRGB()}isUnderlineColorPalette(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()}isUnderlineColorDefault(){return 268435456&this.bg&&~this.extended.underlineColor?0==(50331648&this.extended.underlineColor):this.isFgDefault()}getUnderlineStyle(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0}}t.AttributeData=i;class s{get ext(){return this._urlId?-469762049&this._ext|this.underlineStyle<<26:this._ext}set ext(e){this._ext=e}get underlineStyle(){return this._urlId?5:(469762048&this._ext)>>26}set underlineStyle(e){this._ext&=-469762049,this._ext|=e<<26&469762048}get underlineColor(){return 67108863&this._ext}set underlineColor(e){this._ext&=-67108864,this._ext|=67108863&e}get urlId(){return this._urlId}set urlId(e){this._urlId=e}constructor(e=0,t=0){this._ext=0,this._urlId=0,this._ext=e,this._urlId=t}clone(){return new s(this._ext,this._urlId)}isEmpty(){return 0===this.underlineStyle&&0===this._urlId}}t.ExtendedAttrs=s},9092:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Buffer=t.MAX_BUFFER_SIZE=void 0;const s=i(6349),r=i(7226),n=i(3734),o=i(8437),a=i(4634),h=i(511),c=i(643),l=i(4863),d=i(7116);t.MAX_BUFFER_SIZE=4294967295,t.Buffer=class{constructor(e,t,i){this._hasScrollback=e,this._optionsService=t,this._bufferService=i,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=o.DEFAULT_ATTR_DATA.clone(),this.savedCharset=d.DEFAULT_CHARSET,this.markers=[],this._nullCell=h.CellData.fromCharData([0,c.NULL_CELL_CHAR,c.NULL_CELL_WIDTH,c.NULL_CELL_CODE]),this._whitespaceCell=h.CellData.fromCharData([0,c.WHITESPACE_CELL_CHAR,c.WHITESPACE_CELL_WIDTH,c.WHITESPACE_CELL_CODE]),this._isClearing=!1,this._memoryCleanupQueue=new r.IdleTaskQueue,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new n.ExtendedAttrs),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new n.ExtendedAttrs),this._whitespaceCell}getBlankLine(e,t){return new o.BufferLine(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){const e=this.ybase+this.y-this.ydisp;return e>=0&&et.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:i}fillViewportRows(e){if(0===this.lines.length){void 0===e&&(e=o.DEFAULT_ATTR_DATA);let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){const i=this.getNullCell(o.DEFAULT_ATTR_DATA);let s=0;const r=this._getCorrectBufferLength(t);if(r>this.lines.maxLength&&(this.lines.maxLength=r),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+n+1?(this.ybase--,n++,this.ydisp>0&&this.ydisp--):this.lines.push(new o.BufferLine(e,i)));else for(let e=this._rows;e>t;e--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(r0&&(this.lines.trimStart(e),this.ybase=Math.max(this.ybase-e,0),this.ydisp=Math.max(this.ydisp-e,0),this.savedY=Math.max(this.savedY-e,0)),this.lines.maxLength=r}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),n&&(this.y+=n),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let t=0;t.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue((()=>this._batchedMemoryCleanup())))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition100)return!0;return e}get _isReflowEnabled(){const e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&"conpty"===e.backend&&e.buildNumber>=21376:this._hasScrollback&&!this._optionsService.rawOptions.windowsMode}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){const i=(0,a.reflowLargerGetLinesToRemove)(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(o.DEFAULT_ATTR_DATA));if(i.length>0){const s=(0,a.reflowLargerCreateNewLayout)(this.lines,i);(0,a.reflowLargerApplyNewLayout)(this.lines,s.layout),this._reflowLargerAdjustViewport(e,t,s.countRemoved)}}_reflowLargerAdjustViewport(e,t,i){const s=this.getNullCell(o.DEFAULT_ATTR_DATA);let r=i;for(;r-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length=0;n--){let h=this.lines.get(n);if(!h||!h.isWrapped&&h.getTrimmedLength()<=e)continue;const c=[h];for(;h.isWrapped&&n>0;)h=this.lines.get(--n),c.unshift(h);const l=this.ybase+this.y;if(l>=n&&l0&&(s.push({start:n+c.length+r,newLines:v}),r+=v.length),c.push(...v);let p=_.length-1,g=_[p];0===g&&(p--,g=_[p]);let m=c.length-u-1,S=d;for(;m>=0;){const e=Math.min(S,g);if(void 0===c[p])break;if(c[p].copyCellsFrom(c[m],S-e,g-e,e,!0),g-=e,0===g&&(p--,g=_[p]),S-=e,0===S){m--;const e=Math.max(m,0);S=(0,a.getWrappedLineTrimmedLength)(c,e,this._cols)}}for(let t=0;t0;)0===this.ybase?this.y0){const e=[],t=[];for(let e=0;e=0;c--)if(a&&a.start>n+h){for(let e=a.newLines.length-1;e>=0;e--)this.lines.set(c--,a.newLines[e]);c++,e.push({index:n+1,amount:a.newLines.length}),h+=a.newLines.length,a=s[++o]}else this.lines.set(c,t[n--]);let c=0;for(let t=e.length-1;t>=0;t--)e[t].index+=c,this.lines.onInsertEmitter.fire(e[t]),c+=e[t].amount;const l=Math.max(0,i+r-this.lines.maxLength);l>0&&this.lines.onTrimEmitter.fire(l)}}translateBufferLineToString(e,t,i=0,s){const r=this.lines.get(e);return r?r.translateToString(t,i,s):""}getWrappedRangeForLine(e){let t=e,i=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;i+10;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(null==e&&(e=this.x);!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t{t.line-=e,t.line<0&&t.dispose()}))),t.register(this.lines.onInsert((e=>{t.line>=e.index&&(t.line+=e.amount)}))),t.register(this.lines.onDelete((e=>{t.line>=e.index&&t.linee.index&&(t.line-=e.amount)}))),t.register(t.onDispose((()=>this._removeMarker(t)))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}}},8437:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.BufferLine=t.DEFAULT_ATTR_DATA=void 0;const s=i(3734),r=i(511),n=i(643),o=i(482);t.DEFAULT_ATTR_DATA=Object.freeze(new s.AttributeData);let a=0;class h{constructor(e,t,i=!1){this.isWrapped=i,this._combined={},this._extendedAttrs={},this._data=new Uint32Array(3*e);const s=t||r.CellData.fromCharData([0,n.NULL_CELL_CHAR,n.NULL_CELL_WIDTH,n.NULL_CELL_CODE]);for(let t=0;t>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):i]}set(e,t){this._data[3*e+1]=t[n.CHAR_DATA_ATTR_INDEX],t[n.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[n.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[n.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[n.CHAR_DATA_WIDTH_INDEX]<<22}getWidth(e){return this._data[3*e+0]>>22}hasWidth(e){return 12582912&this._data[3*e+0]}getFg(e){return this._data[3*e+1]}getBg(e){return this._data[3*e+2]}hasContent(e){return 4194303&this._data[3*e+0]}getCodePoint(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t}isCombined(e){return 2097152&this._data[3*e+0]}getString(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?(0,o.stringFromCodePoint)(2097151&t):""}isProtected(e){return 536870912&this._data[3*e+2]}loadCell(e,t){return a=3*e,t.content=this._data[a+0],t.fg=this._data[a+1],t.bg=this._data[a+2],2097152&t.content&&(t.combinedData=this._combined[e]),268435456&t.bg&&(t.extended=this._extendedAttrs[e]),t}setCell(e,t){2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg}setCellFromCodePoint(e,t,i,s,r,n){268435456&r&&(this._extendedAttrs[e]=n),this._data[3*e+0]=t|i<<22,this._data[3*e+1]=s,this._data[3*e+2]=r}addCodepointToCell(e,t){let i=this._data[3*e+0];2097152&i?this._combined[e]+=(0,o.stringFromCodePoint)(t):(2097151&i?(this._combined[e]=(0,o.stringFromCodePoint)(2097151&i)+(0,o.stringFromCodePoint)(t),i&=-2097152,i|=2097152):i=t|1<<22,this._data[3*e+0]=i)}insertCells(e,t,i,n){if((e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==n?void 0:n.fg)||0,(null==n?void 0:n.bg)||0,(null==n?void 0:n.extended)||new s.ExtendedAttrs),t=0;--i)this.setCell(e+t+i,this.loadCell(e+i,s));for(let s=0;sthis.length){if(this._data.buffer.byteLength>=4*i)this._data=new Uint32Array(this._data.buffer,0,i);else{const e=new Uint32Array(i);e.set(this._data),this._data=e}for(let i=this.length;i=e&&delete this._combined[s]}const s=Object.keys(this._extendedAttrs);for(let t=0;t=e&&delete this._extendedAttrs[i]}}return this.length=e,4*i*2=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0}getNoBgTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(4194303&this._data[3*e+0]||50331648&this._data[3*e+2])return e+(this._data[3*e+0]>>22);return 0}copyCellsFrom(e,t,i,s,r){const n=e._data;if(r)for(let r=s-1;r>=0;r--){for(let e=0;e<3;e++)this._data[3*(i+r)+e]=n[3*(t+r)+e];268435456&n[3*(t+r)+2]&&(this._extendedAttrs[i+r]=e._extendedAttrs[t+r])}else for(let r=0;r=t&&(this._combined[r-t+i]=e._combined[r])}}translateToString(e=!1,t=0,i=this.length){e&&(i=Math.min(i,this.getTrimmedLength()));let s="";for(;t>22||1}return s}}t.BufferLine=h},4841:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.getRangeLength=void 0,t.getRangeLength=function(e,t){if(e.start.y>e.end.y)throw new Error(`Buffer range end (${e.end.x}, ${e.end.y}) cannot be before start (${e.start.x}, ${e.start.y})`);return t*(e.end.y-e.start.y)+(e.end.x-e.start.x+1)}},4634:(e,t)=>{function i(e,t,i){if(t===e.length-1)return e[t].getTrimmedLength();const s=!e[t].hasContent(i-1)&&1===e[t].getWidth(i-1),r=2===e[t+1].getWidth(0);return s&&r?i-1:i}Object.defineProperty(t,"__esModule",{value:!0}),t.getWrappedLineTrimmedLength=t.reflowSmallerGetNewLineLengths=t.reflowLargerApplyNewLayout=t.reflowLargerCreateNewLayout=t.reflowLargerGetLinesToRemove=void 0,t.reflowLargerGetLinesToRemove=function(e,t,s,r,n){const o=[];for(let a=0;a=a&&r0&&(e>d||0===l[e].getTrimmedLength());e--)v++;v>0&&(o.push(a+l.length-v),o.push(v)),a+=l.length-1}return o},t.reflowLargerCreateNewLayout=function(e,t){const i=[];let s=0,r=t[s],n=0;for(let o=0;oi(e,r,t))).reduce(((e,t)=>e+t));let o=0,a=0,h=0;for(;hc&&(o-=c,a++);const l=2===e[a].getWidth(o-1);l&&o--;const d=l?s-1:s;r.push(d),h+=d}return r},t.getWrappedLineTrimmedLength=i},5295:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.BufferSet=void 0;const s=i(8460),r=i(844),n=i(9092);class o extends r.Disposable{constructor(e,t){super(),this._optionsService=e,this._bufferService=t,this._onBufferActivate=this.register(new s.EventEmitter),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this.register(this._optionsService.onSpecificOptionChange("scrollback",(()=>this.resize(this._bufferService.cols,this._bufferService.rows)))),this.register(this._optionsService.onSpecificOptionChange("tabStopWidth",(()=>this.setupTabStops())))}reset(){this._normal=new n.Buffer(!0,this._optionsService,this._bufferService),this._normal.fillViewportRows(),this._alt=new n.Buffer(!1,this._optionsService,this._bufferService),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}}t.BufferSet=o},511:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.CellData=void 0;const s=i(482),r=i(643),n=i(3734);class o extends n.AttributeData{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new n.ExtendedAttrs,this.combinedData=""}static fromCharData(e){const t=new o;return t.setFromCharData(e),t}isCombined(){return 2097152&this.content}getWidth(){return this.content>>22}getChars(){return 2097152&this.content?this.combinedData:2097151&this.content?(0,s.stringFromCodePoint)(2097151&this.content):""}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content}setFromCharData(e){this.fg=e[r.CHAR_DATA_ATTR_INDEX],this.bg=0;let t=!1;if(e[r.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[r.CHAR_DATA_CHAR_INDEX].length){const i=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=i&&i<=56319){const s=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=s&&s<=57343?this.content=1024*(i-55296)+s-56320+65536|e[r.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[r.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[r.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[r.CHAR_DATA_WIDTH_INDEX]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}t.CellData=o},643:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.WHITESPACE_CELL_CODE=t.WHITESPACE_CELL_WIDTH=t.WHITESPACE_CELL_CHAR=t.NULL_CELL_CODE=t.NULL_CELL_WIDTH=t.NULL_CELL_CHAR=t.CHAR_DATA_CODE_INDEX=t.CHAR_DATA_WIDTH_INDEX=t.CHAR_DATA_CHAR_INDEX=t.CHAR_DATA_ATTR_INDEX=t.DEFAULT_EXT=t.DEFAULT_ATTR=t.DEFAULT_COLOR=void 0,t.DEFAULT_COLOR=0,t.DEFAULT_ATTR=256|t.DEFAULT_COLOR<<9,t.DEFAULT_EXT=0,t.CHAR_DATA_ATTR_INDEX=0,t.CHAR_DATA_CHAR_INDEX=1,t.CHAR_DATA_WIDTH_INDEX=2,t.CHAR_DATA_CODE_INDEX=3,t.NULL_CELL_CHAR="",t.NULL_CELL_WIDTH=1,t.NULL_CELL_CODE=0,t.WHITESPACE_CELL_CHAR=" ",t.WHITESPACE_CELL_WIDTH=1,t.WHITESPACE_CELL_CODE=32},4863:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Marker=void 0;const s=i(8460),r=i(844);class n{get id(){return this._id}constructor(e){this.line=e,this.isDisposed=!1,this._disposables=[],this._id=n._nextId++,this._onDispose=this.register(new s.EventEmitter),this.onDispose=this._onDispose.event}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),(0,r.disposeArray)(this._disposables),this._disposables.length=0)}register(e){return this._disposables.push(e),e}}t.Marker=n,n._nextId=1},7116:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.DEFAULT_CHARSET=t.CHARSETS=void 0,t.CHARSETS={},t.DEFAULT_CHARSET=t.CHARSETS.B,t.CHARSETS[0]={"`":"◆",a:"▒",b:"␉",c:"␌",d:"␍",e:"␊",f:"°",g:"±",h:"␤",i:"␋",j:"┘",k:"┐",l:"┌",m:"└",n:"┼",o:"⎺",p:"⎻",q:"─",r:"⎼",s:"⎽",t:"├",u:"┤",v:"┴",w:"┬",x:"│",y:"≤",z:"≥","{":"π","|":"≠","}":"£","~":"·"},t.CHARSETS.A={"#":"£"},t.CHARSETS.B=void 0,t.CHARSETS[4]={"#":"£","@":"¾","[":"ij","\\":"½","]":"|","{":"¨","|":"f","}":"¼","~":"´"},t.CHARSETS.C=t.CHARSETS[5]={"[":"Ä","\\":"Ö","]":"Å","^":"Ü","`":"é","{":"ä","|":"ö","}":"å","~":"ü"},t.CHARSETS.R={"#":"£","@":"à","[":"°","\\":"ç","]":"§","{":"é","|":"ù","}":"è","~":"¨"},t.CHARSETS.Q={"@":"à","[":"â","\\":"ç","]":"ê","^":"î","`":"ô","{":"é","|":"ù","}":"è","~":"û"},t.CHARSETS.K={"@":"§","[":"Ä","\\":"Ö","]":"Ü","{":"ä","|":"ö","}":"ü","~":"ß"},t.CHARSETS.Y={"#":"£","@":"§","[":"°","\\":"ç","]":"é","`":"ù","{":"à","|":"ò","}":"è","~":"ì"},t.CHARSETS.E=t.CHARSETS[6]={"@":"Ä","[":"Æ","\\":"Ø","]":"Å","^":"Ü","`":"ä","{":"æ","|":"ø","}":"å","~":"ü"},t.CHARSETS.Z={"#":"£","@":"§","[":"¡","\\":"Ñ","]":"¿","{":"°","|":"ñ","}":"ç"},t.CHARSETS.H=t.CHARSETS[7]={"@":"É","[":"Ä","\\":"Ö","]":"Å","^":"Ü","`":"é","{":"ä","|":"ö","}":"å","~":"ü"},t.CHARSETS["="]={"#":"ù","@":"à","[":"é","\\":"ç","]":"ê","^":"î",_:"è","`":"ô","{":"ä","|":"ö","}":"ü","~":"û"}},2584:(e,t)=>{var i,s,r;Object.defineProperty(t,"__esModule",{value:!0}),t.C1_ESCAPED=t.C1=t.C0=void 0,function(e){e.NUL="\0",e.SOH="",e.STX="",e.ETX="",e.EOT="",e.ENQ="",e.ACK="",e.BEL="",e.BS="\b",e.HT="\t",e.LF="\n",e.VT="\v",e.FF="\f",e.CR="\r",e.SO="",e.SI="",e.DLE="",e.DC1="",e.DC2="",e.DC3="",e.DC4="",e.NAK="",e.SYN="",e.ETB="",e.CAN="",e.EM="",e.SUB="",e.ESC="",e.FS="",e.GS="",e.RS="",e.US="",e.SP=" ",e.DEL=""}(i||(t.C0=i={})),function(e){e.PAD="€",e.HOP="",e.BPH="‚",e.NBH="ƒ",e.IND="„",e.NEL="…",e.SSA="†",e.ESA="‡",e.HTS="ˆ",e.HTJ="‰",e.VTS="Š",e.PLD="‹",e.PLU="Œ",e.RI="",e.SS2="Ž",e.SS3="",e.DCS="",e.PU1="‘",e.PU2="’",e.STS="“",e.CCH="”",e.MW="•",e.SPA="–",e.EPA="—",e.SOS="˜",e.SGCI="™",e.SCI="š",e.CSI="›",e.ST="œ",e.OSC="",e.PM="ž",e.APC="Ÿ"}(s||(t.C1=s={})),function(e){e.ST=`${i.ESC}\\`}(r||(t.C1_ESCAPED=r={}))},7399:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.evaluateKeyboardEvent=void 0;const s=i(2584),r={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["`","~"],219:["[","{"],220:["\\","|"],221:["]","}"],222:["'",'"']};t.evaluateKeyboardEvent=function(e,t,i,n){const o={type:0,cancel:!1,key:void 0},a=(e.shiftKey?1:0)|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:"UIKeyInputUpArrow"===e.key?o.key=t?s.C0.ESC+"OA":s.C0.ESC+"[A":"UIKeyInputLeftArrow"===e.key?o.key=t?s.C0.ESC+"OD":s.C0.ESC+"[D":"UIKeyInputRightArrow"===e.key?o.key=t?s.C0.ESC+"OC":s.C0.ESC+"[C":"UIKeyInputDownArrow"===e.key&&(o.key=t?s.C0.ESC+"OB":s.C0.ESC+"[B");break;case 8:if(e.altKey){o.key=s.C0.ESC+s.C0.DEL;break}o.key=s.C0.DEL;break;case 9:if(e.shiftKey){o.key=s.C0.ESC+"[Z";break}o.key=s.C0.HT,o.cancel=!0;break;case 13:o.key=e.altKey?s.C0.ESC+s.C0.CR:s.C0.CR,o.cancel=!0;break;case 27:o.key=s.C0.ESC,e.altKey&&(o.key=s.C0.ESC+s.C0.ESC),o.cancel=!0;break;case 37:if(e.metaKey)break;a?(o.key=s.C0.ESC+"[1;"+(a+1)+"D",o.key===s.C0.ESC+"[1;3D"&&(o.key=s.C0.ESC+(i?"b":"[1;5D"))):o.key=t?s.C0.ESC+"OD":s.C0.ESC+"[D";break;case 39:if(e.metaKey)break;a?(o.key=s.C0.ESC+"[1;"+(a+1)+"C",o.key===s.C0.ESC+"[1;3C"&&(o.key=s.C0.ESC+(i?"f":"[1;5C"))):o.key=t?s.C0.ESC+"OC":s.C0.ESC+"[C";break;case 38:if(e.metaKey)break;a?(o.key=s.C0.ESC+"[1;"+(a+1)+"A",i||o.key!==s.C0.ESC+"[1;3A"||(o.key=s.C0.ESC+"[1;5A")):o.key=t?s.C0.ESC+"OA":s.C0.ESC+"[A";break;case 40:if(e.metaKey)break;a?(o.key=s.C0.ESC+"[1;"+(a+1)+"B",i||o.key!==s.C0.ESC+"[1;3B"||(o.key=s.C0.ESC+"[1;5B")):o.key=t?s.C0.ESC+"OB":s.C0.ESC+"[B";break;case 45:e.shiftKey||e.ctrlKey||(o.key=s.C0.ESC+"[2~");break;case 46:o.key=a?s.C0.ESC+"[3;"+(a+1)+"~":s.C0.ESC+"[3~";break;case 36:o.key=a?s.C0.ESC+"[1;"+(a+1)+"H":t?s.C0.ESC+"OH":s.C0.ESC+"[H";break;case 35:o.key=a?s.C0.ESC+"[1;"+(a+1)+"F":t?s.C0.ESC+"OF":s.C0.ESC+"[F";break;case 33:e.shiftKey?o.type=2:e.ctrlKey?o.key=s.C0.ESC+"[5;"+(a+1)+"~":o.key=s.C0.ESC+"[5~";break;case 34:e.shiftKey?o.type=3:e.ctrlKey?o.key=s.C0.ESC+"[6;"+(a+1)+"~":o.key=s.C0.ESC+"[6~";break;case 112:o.key=a?s.C0.ESC+"[1;"+(a+1)+"P":s.C0.ESC+"OP";break;case 113:o.key=a?s.C0.ESC+"[1;"+(a+1)+"Q":s.C0.ESC+"OQ";break;case 114:o.key=a?s.C0.ESC+"[1;"+(a+1)+"R":s.C0.ESC+"OR";break;case 115:o.key=a?s.C0.ESC+"[1;"+(a+1)+"S":s.C0.ESC+"OS";break;case 116:o.key=a?s.C0.ESC+"[15;"+(a+1)+"~":s.C0.ESC+"[15~";break;case 117:o.key=a?s.C0.ESC+"[17;"+(a+1)+"~":s.C0.ESC+"[17~";break;case 118:o.key=a?s.C0.ESC+"[18;"+(a+1)+"~":s.C0.ESC+"[18~";break;case 119:o.key=a?s.C0.ESC+"[19;"+(a+1)+"~":s.C0.ESC+"[19~";break;case 120:o.key=a?s.C0.ESC+"[20;"+(a+1)+"~":s.C0.ESC+"[20~";break;case 121:o.key=a?s.C0.ESC+"[21;"+(a+1)+"~":s.C0.ESC+"[21~";break;case 122:o.key=a?s.C0.ESC+"[23;"+(a+1)+"~":s.C0.ESC+"[23~";break;case 123:o.key=a?s.C0.ESC+"[24;"+(a+1)+"~":s.C0.ESC+"[24~";break;default:if(!e.ctrlKey||e.shiftKey||e.altKey||e.metaKey)if(i&&!n||!e.altKey||e.metaKey)!i||e.altKey||e.ctrlKey||e.shiftKey||!e.metaKey?e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&1===e.key.length?o.key=e.key:e.key&&e.ctrlKey&&("_"===e.key&&(o.key=s.C0.US),"@"===e.key&&(o.key=s.C0.NUL)):65===e.keyCode&&(o.type=1);else{const t=r[e.keyCode],i=null==t?void 0:t[e.shiftKey?1:0];if(i)o.key=s.C0.ESC+i;else if(e.keyCode>=65&&e.keyCode<=90){const t=e.ctrlKey?e.keyCode-64:e.keyCode+32;let i=String.fromCharCode(t);e.shiftKey&&(i=i.toUpperCase()),o.key=s.C0.ESC+i}else if(32===e.keyCode)o.key=s.C0.ESC+(e.ctrlKey?s.C0.NUL:" ");else if("Dead"===e.key&&e.code.startsWith("Key")){let t=e.code.slice(3,4);e.shiftKey||(t=t.toLowerCase()),o.key=s.C0.ESC+t,o.cancel=!0}}else e.keyCode>=65&&e.keyCode<=90?o.key=String.fromCharCode(e.keyCode-64):32===e.keyCode?o.key=s.C0.NUL:e.keyCode>=51&&e.keyCode<=55?o.key=String.fromCharCode(e.keyCode-51+27):56===e.keyCode?o.key=s.C0.DEL:219===e.keyCode?o.key=s.C0.ESC:220===e.keyCode?o.key=s.C0.FS:221===e.keyCode&&(o.key=s.C0.GS)}return o}},482:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=t.utf32ToString=t.stringFromCodePoint=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t=0,i=e.length){let s="";for(let r=t;r65535?(t-=65536,s+=String.fromCharCode(55296+(t>>10))+String.fromCharCode(t%1024+56320)):s+=String.fromCharCode(t)}return s},t.StringToUtf32=class{constructor(){this._interim=0}clear(){this._interim=0}decode(e,t){const i=e.length;if(!i)return 0;let s=0,r=0;if(this._interim){const i=e.charCodeAt(r++);56320<=i&&i<=57343?t[s++]=1024*(this._interim-55296)+i-56320+65536:(t[s++]=this._interim,t[s++]=i),this._interim=0}for(let n=r;n=i)return this._interim=r,s;const o=e.charCodeAt(n);56320<=o&&o<=57343?t[s++]=1024*(r-55296)+o-56320+65536:(t[s++]=r,t[s++]=o)}else 65279!==r&&(t[s++]=r)}return s}},t.Utf8ToUtf32=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(e,t){const i=e.length;if(!i)return 0;let s,r,n,o,a=0,h=0,c=0;if(this.interim[0]){let s=!1,r=this.interim[0];r&=192==(224&r)?31:224==(240&r)?15:7;let n,o=0;for(;(n=63&this.interim[++o])&&o<4;)r<<=6,r|=n;const h=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,l=h-o;for(;c=i)return 0;if(n=e[c++],128!=(192&n)){c--,s=!0;break}this.interim[o++]=n,r<<=6,r|=63&n}s||(2===h?r<128?c--:t[a++]=r:3===h?r<2048||r>=55296&&r<=57343||65279===r||(t[a++]=r):r<65536||r>1114111||(t[a++]=r)),this.interim.fill(0)}const l=i-4;let d=c;for(;d=i)return this.interim[0]=s,a;if(r=e[d++],128!=(192&r)){d--;continue}if(h=(31&s)<<6|63&r,h<128){d--;continue}t[a++]=h}else if(224==(240&s)){if(d>=i)return this.interim[0]=s,a;if(r=e[d++],128!=(192&r)){d--;continue}if(d>=i)return this.interim[0]=s,this.interim[1]=r,a;if(n=e[d++],128!=(192&n)){d--;continue}if(h=(15&s)<<12|(63&r)<<6|63&n,h<2048||h>=55296&&h<=57343||65279===h)continue;t[a++]=h}else if(240==(248&s)){if(d>=i)return this.interim[0]=s,a;if(r=e[d++],128!=(192&r)){d--;continue}if(d>=i)return this.interim[0]=s,this.interim[1]=r,a;if(n=e[d++],128!=(192&n)){d--;continue}if(d>=i)return this.interim[0]=s,this.interim[1]=r,this.interim[2]=n,a;if(o=e[d++],128!=(192&o)){d--;continue}if(h=(7&s)<<18|(63&r)<<12|(63&n)<<6|63&o,h<65536||h>1114111)continue;t[a++]=h}}return a}}},225:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;const i=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],s=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];let r;t.UnicodeV6=class{constructor(){if(this.version="6",!r){r=new Uint8Array(65536),r.fill(1),r[0]=0,r.fill(0,1,32),r.fill(0,127,160),r.fill(2,4352,4448),r[9001]=2,r[9002]=2,r.fill(2,11904,42192),r[12351]=1,r.fill(2,44032,55204),r.fill(2,63744,64256),r.fill(2,65040,65050),r.fill(2,65072,65136),r.fill(2,65280,65377),r.fill(2,65504,65511);for(let e=0;et[r][1])return!1;for(;r>=s;)if(i=s+r>>1,e>t[i][1])s=i+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1}}},5981:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.WriteBuffer=void 0;const s=i(8460),r=i(844);class n extends r.Disposable{constructor(e){super(),this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._onWriteParsed=this.register(new s.EventEmitter),this.onWriteParsed=this._onWriteParsed.event}handleUserInput(){this._didUserInput=!0}writeSync(e,t){if(void 0!==t&&this._syncCalls>t)return void(this._syncCalls=0);if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;let i;for(this._isSyncWriting=!0;i=this._writeBuffer.shift();){this._action(i);const e=this._callbacks.shift();e&&e()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(this._pendingData>5e7)throw new Error("write data discarded, use flow control to avoid losing data");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput)return this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),void this._innerWrite();setTimeout((()=>this._innerWrite()))}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}_innerWrite(e=0,t=!0){const i=e||Date.now();for(;this._writeBuffer.length>this._bufferOffset;){const e=this._writeBuffer[this._bufferOffset],s=this._action(e,t);if(s){const e=e=>Date.now()-i>=12?setTimeout((()=>this._innerWrite(0,e))):this._innerWrite(i,e);return void s.catch((e=>(queueMicrotask((()=>{throw e})),Promise.resolve(!1)))).then(e)}const r=this._callbacks[this._bufferOffset];if(r&&r(),this._bufferOffset++,this._pendingData-=e.length,Date.now()-i>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout((()=>this._innerWrite()))):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}}t.WriteBuffer=n},5941:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.toRgbString=t.parseColor=void 0;const i=/^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/,s=/^[\da-f]+$/;function r(e,t){const i=e.toString(16),s=i.length<2?"0"+i:i;switch(t){case 4:return i[0];case 8:return s;case 12:return(s+s).slice(0,3);default:return s+s}}t.parseColor=function(e){if(!e)return;let t=e.toLowerCase();if(0===t.indexOf("rgb:")){t=t.slice(4);const e=i.exec(t);if(e){const t=e[1]?15:e[4]?255:e[7]?4095:65535;return[Math.round(parseInt(e[1]||e[4]||e[7]||e[10],16)/t*255),Math.round(parseInt(e[2]||e[5]||e[8]||e[11],16)/t*255),Math.round(parseInt(e[3]||e[6]||e[9]||e[12],16)/t*255)]}}else if(0===t.indexOf("#")&&(t=t.slice(1),s.exec(t)&&[3,6,9,12].includes(t.length))){const e=t.length/3,i=[0,0,0];for(let s=0;s<3;++s){const r=parseInt(t.slice(e*s,e*s+e),16);i[s]=1===e?r<<4:2===e?r:3===e?r>>4:r>>8}return i}},t.toRgbString=function(e,t=16){const[i,s,n]=e;return`rgb:${r(i,t)}/${r(s,t)}/${r(n,t)}`}},5770:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.PAYLOAD_LIMIT=void 0,t.PAYLOAD_LIMIT=1e7},6351:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.DcsHandler=t.DcsParser=void 0;const s=i(482),r=i(8742),n=i(5770),o=[];t.DcsParser=class{constructor(){this._handlers=Object.create(null),this._active=o,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=o}registerHandler(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].unhook(!1);this._stack.paused=!1,this._active=o,this._ident=0}hook(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||o,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].hook(t);else this._handlerFb(this._ident,"HOOK",t)}put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._ident,"PUT",(0,s.utf32ToString)(e,t,i))}unhook(e,t=!0){if(this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].unhook(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].unhook(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._ident,"UNHOOK",e);this._active=o,this._ident=0}};const a=new r.Params;a.addParam(0),t.DcsHandler=class{constructor(e){this._handler=e,this._data="",this._params=a,this._hitLimit=!1}hook(e){this._params=e.length>1||e.params[0]?e.clone():a,this._data="",this._hitLimit=!1}put(e,t,i){this._hitLimit||(this._data+=(0,s.utf32ToString)(e,t,i),this._data.length>n.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))}unhook(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data,this._params),t instanceof Promise))return t.then((e=>(this._params=a,this._data="",this._hitLimit=!1,e)));return this._params=a,this._data="",this._hitLimit=!1,t}}},2015:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;const s=i(844),r=i(8742),n=i(6242),o=i(6351);class a{constructor(e){this.table=new Uint8Array(e)}setDefault(e,t){this.table.fill(e<<4|t)}add(e,t,i,s){this.table[t<<8|e]=i<<4|s}addMany(e,t,i,s){for(let r=0;rt)),i=(e,i)=>t.slice(e,i),s=i(32,127),r=i(0,24);r.push(25),r.push.apply(r,i(28,32));const n=i(0,14);let o;for(o in e.setDefault(1,0),e.addMany(s,0,2,0),n)e.addMany([24,26,153,154],o,3,0),e.addMany(i(128,144),o,3,0),e.addMany(i(144,152),o,3,0),e.add(156,o,0,0),e.add(27,o,11,1),e.add(157,o,4,8),e.addMany([152,158,159],o,0,7),e.add(155,o,11,3),e.add(144,o,11,9);return e.addMany(r,0,3,0),e.addMany(r,1,3,1),e.add(127,1,0,1),e.addMany(r,8,0,8),e.addMany(r,3,3,3),e.add(127,3,0,3),e.addMany(r,4,3,4),e.add(127,4,0,4),e.addMany(r,6,3,6),e.addMany(r,5,3,5),e.add(127,5,0,5),e.addMany(r,2,3,2),e.add(127,2,0,2),e.add(93,1,4,8),e.addMany(s,8,5,8),e.add(127,8,5,8),e.addMany([156,27,24,26,7],8,6,0),e.addMany(i(28,32),8,0,8),e.addMany([88,94,95],1,0,7),e.addMany(s,7,0,7),e.addMany(r,7,0,7),e.add(156,7,0,0),e.add(127,7,0,7),e.add(91,1,11,3),e.addMany(i(64,127),3,7,0),e.addMany(i(48,60),3,8,4),e.addMany([60,61,62,63],3,9,4),e.addMany(i(48,60),4,8,4),e.addMany(i(64,127),4,7,0),e.addMany([60,61,62,63],4,0,6),e.addMany(i(32,64),6,0,6),e.add(127,6,0,6),e.addMany(i(64,127),6,0,0),e.addMany(i(32,48),3,9,5),e.addMany(i(32,48),5,9,5),e.addMany(i(48,64),5,0,6),e.addMany(i(64,127),5,7,0),e.addMany(i(32,48),4,9,5),e.addMany(i(32,48),1,9,2),e.addMany(i(32,48),2,9,2),e.addMany(i(48,127),2,10,0),e.addMany(i(48,80),1,10,0),e.addMany(i(81,88),1,10,0),e.addMany([89,90,92],1,10,0),e.addMany(i(96,127),1,10,0),e.add(80,1,11,9),e.addMany(r,9,0,9),e.add(127,9,0,9),e.addMany(i(28,32),9,0,9),e.addMany(i(32,48),9,9,12),e.addMany(i(48,60),9,8,10),e.addMany([60,61,62,63],9,9,10),e.addMany(r,11,0,11),e.addMany(i(32,128),11,0,11),e.addMany(i(28,32),11,0,11),e.addMany(r,10,0,10),e.add(127,10,0,10),e.addMany(i(28,32),10,0,10),e.addMany(i(48,60),10,8,10),e.addMany([60,61,62,63],10,0,11),e.addMany(i(32,48),10,9,12),e.addMany(r,12,0,12),e.add(127,12,0,12),e.addMany(i(28,32),12,0,12),e.addMany(i(32,48),12,9,12),e.addMany(i(48,64),12,0,11),e.addMany(i(64,127),12,12,13),e.addMany(i(64,127),10,12,13),e.addMany(i(64,127),9,12,13),e.addMany(r,13,13,13),e.addMany(s,13,13,13),e.add(127,13,0,13),e.addMany([27,156,24,26],13,14,0),e.add(h,0,2,0),e.add(h,8,5,8),e.add(h,6,0,6),e.add(h,11,0,11),e.add(h,13,13,13),e}();class c extends s.Disposable{constructor(e=t.VT500_TRANSITION_TABLE){super(),this._transitions=e,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new r.Params,this._params.addParam(0),this._collect=0,this.precedingCodepoint=0,this._printHandlerFb=(e,t,i)=>{},this._executeHandlerFb=e=>{},this._csiHandlerFb=(e,t)=>{},this._escHandlerFb=e=>{},this._errorHandlerFb=e=>e,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this.register((0,s.toDisposable)((()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null)}))),this._oscParser=this.register(new n.OscParser),this._dcsParser=this.register(new o.DcsParser),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:"\\"},(()=>!0))}_identifier(e,t=[64,126]){let i=0;if(e.prefix){if(e.prefix.length>1)throw new Error("only one byte as prefix supported");if(i=e.prefix.charCodeAt(0),i&&60>i||i>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(let t=0;ts||s>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");i<<=8,i|=s}}if(1!==e.final.length)throw new Error("final must be a single byte");const s=e.final.charCodeAt(0);if(t[0]>s||s>t[1])throw new Error(`final must be in range ${t[0]} .. ${t[1]}`);return i<<=8,i|=s,i}identToString(e){const t=[];for(;e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join("")}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){const i=this._identifier(e,[48,126]);void 0===this._escHandlers[i]&&(this._escHandlers[i]=[]);const s=this._escHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){this._executeHandlers[e.charCodeAt(0)]=t}clearExecuteHandler(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){const i=this._identifier(e);void 0===this._csiHandlers[i]&&(this._csiHandlers[i]=[]);const s=this._csiHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0,0!==this._parseStack.state&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,i,s,r){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=i,this._parseStack.transition=s,this._parseStack.chunkPos=r}parse(e,t,i){let s,r=0,n=0,o=0;if(this._parseStack.state)if(2===this._parseStack.state)this._parseStack.state=0,o=this._parseStack.chunkPos+1;else{if(void 0===i||1===this._parseStack.state)throw this._parseStack.state=1,new Error("improper continuation due to previous async handler, giving up parsing");const t=this._parseStack.handlers;let n=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(!1===i&&n>-1)for(;n>=0&&(s=t[n](this._params),!0!==s);n--)if(s instanceof Promise)return this._parseStack.handlerPos=n,s;this._parseStack.handlers=[];break;case 4:if(!1===i&&n>-1)for(;n>=0&&(s=t[n](),!0!==s);n--)if(s instanceof Promise)return this._parseStack.handlerPos=n,s;this._parseStack.handlers=[];break;case 6:if(r=e[this._parseStack.chunkPos],s=this._dcsParser.unhook(24!==r&&26!==r,i),s)return s;27===r&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break;case 5:if(r=e[this._parseStack.chunkPos],s=this._oscParser.end(24!==r&&26!==r,i),s)return s;27===r&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0}this._parseStack.state=0,o=this._parseStack.chunkPos+1,this.precedingCodepoint=0,this.currentState=15&this._parseStack.transition}for(let i=o;i>4){case 2:for(let s=i+1;;++s){if(s>=t||(r=e[s])<32||r>126&&r=t||(r=e[s])<32||r>126&&r=t||(r=e[s])<32||r>126&&r=t||(r=e[s])<32||r>126&&r=0&&(s=o[a](this._params),!0!==s);a--)if(s instanceof Promise)return this._preserveStack(3,o,a,n,i),s;a<0&&this._csiHandlerFb(this._collect<<8|r,this._params),this.precedingCodepoint=0;break;case 8:do{switch(r){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(r-48)}}while(++i47&&r<60);i--;break;case 9:this._collect<<=8,this._collect|=r;break;case 10:const c=this._escHandlers[this._collect<<8|r];let l=c?c.length-1:-1;for(;l>=0&&(s=c[l](),!0!==s);l--)if(s instanceof Promise)return this._preserveStack(4,c,l,n,i),s;l<0&&this._escHandlerFb(this._collect<<8|r),this.precedingCodepoint=0;break;case 11:this._params.reset(),this._params.addParam(0),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|r,this._params);break;case 13:for(let s=i+1;;++s)if(s>=t||24===(r=e[s])||26===r||27===r||r>127&&r=t||(r=e[s])<32||r>127&&r{Object.defineProperty(t,"__esModule",{value:!0}),t.OscHandler=t.OscParser=void 0;const s=i(5770),r=i(482),n=[];t.OscParser=class{constructor(){this._state=0,this._active=n,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=n}reset(){if(2===this._state)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=n,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||n,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].start();else this._handlerFb(this._id,"START")}_put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._id,"PUT",(0,r.utf32ToString)(e,t,i))}start(){this.reset(),this._state=1}put(e,t,i){if(3!==this._state){if(1===this._state)for(;t0&&this._put(e,t,i)}}end(e,t=!0){if(0!==this._state){if(3!==this._state)if(1===this._state&&this._start(),this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].end(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].end(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._id,"END",e);this._active=n,this._id=-1,this._state=0}}},t.OscHandler=class{constructor(e){this._handler=e,this._data="",this._hitLimit=!1}start(){this._data="",this._hitLimit=!1}put(e,t,i){this._hitLimit||(this._data+=(0,r.utf32ToString)(e,t,i),this._data.length>s.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data),t instanceof Promise))return t.then((e=>(this._data="",this._hitLimit=!1,e)));return this._data="",this._hitLimit=!1,t}}},8742:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Params=void 0;const i=2147483647;class s{static fromArray(e){const t=new s;if(!e.length)return t;for(let i=Array.isArray(e[0])?1:0;i256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}clone(){const e=new s(this.maxLength,this.maxSubParamsLength);return e.params.set(this.params),e.length=this.length,e._subParams.set(this._subParams),e._subParamsLength=this._subParamsLength,e._subParamsIdx.set(this._subParamsIdx),e._rejectDigits=this._rejectDigits,e._rejectSubDigits=this._rejectSubDigits,e._digitIsSub=this._digitIsSub,e}toArray(){const e=[];for(let t=0;t>8,s=255&this._subParamsIdx[t];s-i>0&&e.push(Array.prototype.slice.call(this._subParams,i,s))}return e}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}addParam(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>i?i:e}}addSubParam(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParams[this._subParamsLength++]=e>i?i:e,this._subParamsIdx[this.length-1]++}}hasSubParams(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0}getSubParams(e){const t=this._subParamsIdx[e]>>8,i=255&this._subParamsIdx[e];return i-t>0?this._subParams.subarray(t,i):null}getSubParamsAll(){const e={};for(let t=0;t>8,s=255&this._subParamsIdx[t];s-i>0&&(e[t]=this._subParams.slice(i,s))}return e}addDigit(e){let t;if(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;const s=this._digitIsSub?this._subParams:this.params,r=s[t-1];s[t-1]=~r?Math.min(10*r+e,i):e}}t.Params=s},5741:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.AddonManager=void 0,t.AddonManager=class{constructor(){this._addons=[]}dispose(){for(let e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()}loadAddon(e,t){const i={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(i),t.dispose=()=>this._wrappedAddonDispose(i),t.activate(e)}_wrappedAddonDispose(e){if(e.isDisposed)return;let t=-1;for(let i=0;i{Object.defineProperty(t,"__esModule",{value:!0}),t.BufferApiView=void 0;const s=i(3785),r=i(511);t.BufferApiView=class{constructor(e,t){this._buffer=e,this.type=t}init(e){return this._buffer=e,this}get cursorY(){return this._buffer.y}get cursorX(){return this._buffer.x}get viewportY(){return this._buffer.ydisp}get baseY(){return this._buffer.ybase}get length(){return this._buffer.lines.length}getLine(e){const t=this._buffer.lines.get(e);if(t)return new s.BufferLineApiView(t)}getNullCell(){return new r.CellData}}},3785:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.BufferLineApiView=void 0;const s=i(511);t.BufferLineApiView=class{constructor(e){this._line=e}get isWrapped(){return this._line.isWrapped}get length(){return this._line.length}getCell(e,t){if(!(e<0||e>=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new s.CellData)}translateToString(e,t,i){return this._line.translateToString(e,t,i)}}},8285:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.BufferNamespaceApi=void 0;const s=i(8771),r=i(8460),n=i(844);class o extends n.Disposable{constructor(e){super(),this._core=e,this._onBufferChange=this.register(new r.EventEmitter),this.onBufferChange=this._onBufferChange.event,this._normal=new s.BufferApiView(this._core.buffers.normal,"normal"),this._alternate=new s.BufferApiView(this._core.buffers.alt,"alternate"),this._core.buffers.onBufferActivate((()=>this._onBufferChange.fire(this.active)))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw new Error("Active buffer is neither normal nor alternate")}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}}t.BufferNamespaceApi=o},7975:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ParserApi=void 0,t.ParserApi=class{constructor(e){this._core=e}registerCsiHandler(e,t){return this._core.registerCsiHandler(e,(e=>t(e.toArray())))}addCsiHandler(e,t){return this.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._core.registerDcsHandler(e,((e,i)=>t(e,i.toArray())))}addDcsHandler(e,t){return this.registerDcsHandler(e,t)}registerEscHandler(e,t){return this._core.registerEscHandler(e,t)}addEscHandler(e,t){return this.registerEscHandler(e,t)}registerOscHandler(e,t){return this._core.registerOscHandler(e,t)}addOscHandler(e,t){return this.registerOscHandler(e,t)}}},7090:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeApi=void 0,t.UnicodeApi=class{constructor(e){this._core=e}register(e){this._core.unicodeService.register(e)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(e){this._core.unicodeService.activeVersion=e}}},744:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferService=t.MINIMUM_ROWS=t.MINIMUM_COLS=void 0;const n=i(8460),o=i(844),a=i(5295),h=i(2585);t.MINIMUM_COLS=2,t.MINIMUM_ROWS=1;let c=t.BufferService=class extends o.Disposable{get buffer(){return this.buffers.active}constructor(e){super(),this.isUserScrolling=!1,this._onResize=this.register(new n.EventEmitter),this.onResize=this._onResize.event,this._onScroll=this.register(new n.EventEmitter),this.onScroll=this._onScroll.event,this.cols=Math.max(e.rawOptions.cols||0,t.MINIMUM_COLS),this.rows=Math.max(e.rawOptions.rows||0,t.MINIMUM_ROWS),this.buffers=this.register(new a.BufferSet(e,this))}resize(e,t){this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){const i=this.buffer;let s;s=this._cachedBlankLine,s&&s.length===this.cols&&s.getFg(0)===e.fg&&s.getBg(0)===e.bg||(s=i.getBlankLine(e,t),this._cachedBlankLine=s),s.isWrapped=t;const r=i.ybase+i.scrollTop,n=i.ybase+i.scrollBottom;if(0===i.scrollTop){const e=i.lines.isFull;n===i.lines.length-1?e?i.lines.recycle().copyFrom(s):i.lines.push(s.clone()):i.lines.splice(n+1,0,s.clone()),e?this.isUserScrolling&&(i.ydisp=Math.max(i.ydisp-1,0)):(i.ybase++,this.isUserScrolling||i.ydisp++)}else{const e=n-r+1;i.lines.shiftElements(r+1,e-1,-1),i.lines.set(n,s.clone())}this.isUserScrolling||(i.ydisp=i.ybase),this._onScroll.fire(i.ydisp)}scrollLines(e,t,i){const s=this.buffer;if(e<0){if(0===s.ydisp)return;this.isUserScrolling=!0}else e+s.ydisp>=s.ybase&&(this.isUserScrolling=!1);const r=s.ydisp;s.ydisp=Math.max(Math.min(s.ydisp+e,s.ybase),0),r!==s.ydisp&&(t||this._onScroll.fire(s.ydisp))}};t.BufferService=c=s([r(0,h.IOptionsService)],c)},7994:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.CharsetService=void 0,t.CharsetService=class{constructor(){this.glevel=0,this._charsets=[]}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(e){this.glevel=e,this.charset=this._charsets[e]}setgCharset(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)}}},1753:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreMouseService=void 0;const n=i(2585),o=i(8460),a=i(844),h={NONE:{events:0,restrict:()=>!1},X10:{events:1,restrict:e=>4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)},VT200:{events:19,restrict:e=>32!==e.action},DRAG:{events:23,restrict:e=>32!==e.action||3!==e.button},ANY:{events:31,restrict:e=>!0}};function c(e,t){let i=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(i|=64,i|=e.action):(i|=3&e.button,4&e.button&&(i|=64),8&e.button&&(i|=128),32===e.action?i|=32:0!==e.action||t||(i|=3)),i}const l=String.fromCharCode,d={DEFAULT:e=>{const t=[c(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?"":`${l(t[0])}${l(t[1])}${l(t[2])}`},SGR:e=>{const t=0===e.action&&4!==e.button?"m":"M";return`[<${c(e,!0)};${e.col};${e.row}${t}`},SGR_PIXELS:e=>{const t=0===e.action&&4!==e.button?"m":"M";return`[<${c(e,!0)};${e.x};${e.y}${t}`}};let _=t.CoreMouseService=class extends a.Disposable{constructor(e,t){super(),this._bufferService=e,this._coreService=t,this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._lastEvent=null,this._onProtocolChange=this.register(new o.EventEmitter),this.onProtocolChange=this._onProtocolChange.event;for(const e of Object.keys(h))this.addProtocol(e,h[e]);for(const e of Object.keys(d))this.addEncoding(e,d[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return 0!==this._protocols[this._activeProtocol].events}set activeProtocol(e){if(!this._protocols[e])throw new Error(`unknown protocol "${e}"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw new Error(`unknown encoding "${e}"`);this._activeEncoding=e}reset(){this.activeProtocol="NONE",this.activeEncoding="DEFAULT",this._lastEvent=null}triggerMouseEvent(e){if(e.col<0||e.col>=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows)return!1;if(4===e.button&&32===e.action)return!1;if(3===e.button&&32!==e.action)return!1;if(4!==e.button&&(2===e.action||3===e.action))return!1;if(e.col++,e.row++,32===e.action&&this._lastEvent&&this._equalEvents(this._lastEvent,e,"SGR_PIXELS"===this._activeEncoding))return!1;if(!this._protocols[this._activeProtocol].restrict(e))return!1;const t=this._encodings[this._activeEncoding](e);return t&&("DEFAULT"===this._activeEncoding?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0}explainEvents(e){return{down:!!(1&e),up:!!(2&e),drag:!!(4&e),move:!!(8&e),wheel:!!(16&e)}}_equalEvents(e,t,i){if(i){if(e.x!==t.x)return!1;if(e.y!==t.y)return!1}else{if(e.col!==t.col)return!1;if(e.row!==t.row)return!1}return e.button===t.button&&e.action===t.action&&e.ctrl===t.ctrl&&e.alt===t.alt&&e.shift===t.shift}};t.CoreMouseService=_=s([r(0,n.IBufferService),r(1,n.ICoreService)],_)},6975:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreService=void 0;const n=i(1439),o=i(8460),a=i(844),h=i(2585),c=Object.freeze({insertMode:!1}),l=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,origin:!1,reverseWraparound:!1,sendFocus:!1,wraparound:!0});let d=t.CoreService=class extends a.Disposable{constructor(e,t,i){super(),this._bufferService=e,this._logService=t,this._optionsService=i,this.isCursorInitialized=!1,this.isCursorHidden=!1,this._onData=this.register(new o.EventEmitter),this.onData=this._onData.event,this._onUserInput=this.register(new o.EventEmitter),this.onUserInput=this._onUserInput.event,this._onBinary=this.register(new o.EventEmitter),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this.register(new o.EventEmitter),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.modes=(0,n.clone)(c),this.decPrivateModes=(0,n.clone)(l)}reset(){this.modes=(0,n.clone)(c),this.decPrivateModes=(0,n.clone)(l)}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;const i=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&i.ybase!==i.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data "${e}"`,(()=>e.split("").map((e=>e.charCodeAt(0))))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${e}"`,(()=>e.split("").map((e=>e.charCodeAt(0))))),this._onBinary.fire(e))}};t.CoreService=d=s([r(0,h.IBufferService),r(1,h.ILogService),r(2,h.IOptionsService)],d)},9074:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.DecorationService=void 0;const s=i(8055),r=i(8460),n=i(844),o=i(6106);let a=0,h=0;class c extends n.Disposable{get decorations(){return this._decorations.values()}constructor(){super(),this._decorations=new o.SortedList((e=>null==e?void 0:e.marker.line)),this._onDecorationRegistered=this.register(new r.EventEmitter),this.onDecorationRegistered=this._onDecorationRegistered.event,this._onDecorationRemoved=this.register(new r.EventEmitter),this.onDecorationRemoved=this._onDecorationRemoved.event,this.register((0,n.toDisposable)((()=>this.reset())))}registerDecoration(e){if(e.marker.isDisposed)return;const t=new l(e);if(t){const e=t.marker.onDispose((()=>t.dispose()));t.onDispose((()=>{t&&(this._decorations.delete(t)&&this._onDecorationRemoved.fire(t),e.dispose())})),this._decorations.insert(t),this._onDecorationRegistered.fire(t)}return t}reset(){for(const e of this._decorations.values())e.dispose();this._decorations.clear()}*getDecorationsAtCell(e,t,i){var s,r,n;let o=0,a=0;for(const h of this._decorations.getKeyIterator(t))o=null!==(s=h.options.x)&&void 0!==s?s:0,a=o+(null!==(r=h.options.width)&&void 0!==r?r:1),e>=o&&e{var r,n,o;a=null!==(r=t.options.x)&&void 0!==r?r:0,h=a+(null!==(n=t.options.width)&&void 0!==n?n:1),e>=a&&e{Object.defineProperty(t,"__esModule",{value:!0}),t.InstantiationService=t.ServiceCollection=void 0;const s=i(2585),r=i(8343);class n{constructor(...e){this._entries=new Map;for(const[t,i]of e)this.set(t,i)}set(e,t){const i=this._entries.get(e);return this._entries.set(e,t),i}forEach(e){for(const[t,i]of this._entries.entries())e(t,i)}has(e){return this._entries.has(e)}get(e){return this._entries.get(e)}}t.ServiceCollection=n,t.InstantiationService=class{constructor(){this._services=new n,this._services.set(s.IInstantiationService,this)}setService(e,t){this._services.set(e,t)}getService(e){return this._services.get(e)}createInstance(e,...t){const i=(0,r.getServiceDependencies)(e).sort(((e,t)=>e.index-t.index)),s=[];for(const t of i){const i=this._services.get(t.id);if(!i)throw new Error(`[createInstance] ${e.name} depends on UNKNOWN service ${t.id}.`);s.push(i)}const n=i.length>0?i[0].index:t.length;if(t.length!==n)throw new Error(`[createInstance] First service dependency of ${e.name} at position ${n+1} conflicts with ${t.length} static arguments`);return new e(...[...t,...s])}}},7866:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.traceCall=t.setTraceLogger=t.LogService=void 0;const n=i(844),o=i(2585),a={trace:o.LogLevelEnum.TRACE,debug:o.LogLevelEnum.DEBUG,info:o.LogLevelEnum.INFO,warn:o.LogLevelEnum.WARN,error:o.LogLevelEnum.ERROR,off:o.LogLevelEnum.OFF};let h,c=t.LogService=class extends n.Disposable{get logLevel(){return this._logLevel}constructor(e){super(),this._optionsService=e,this._logLevel=o.LogLevelEnum.OFF,this._updateLogLevel(),this.register(this._optionsService.onSpecificOptionChange("logLevel",(()=>this._updateLogLevel()))),h=this}_updateLogLevel(){this._logLevel=a[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;tJSON.stringify(e))).join(", ")})`);const t=s.apply(this,e);return h.trace(`GlyphRenderer#${s.name} return`,t),t}}},7302:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.OptionsService=t.DEFAULT_OPTIONS=void 0;const s=i(8460),r=i(844),n=i(6114);t.DEFAULT_OPTIONS={cols:80,rows:24,cursorBlink:!1,cursorStyle:"block",cursorWidth:1,cursorInactiveStyle:"outline",customGlyphs:!0,drawBoldTextInBrightColors:!0,fastScrollModifier:"alt",fastScrollSensitivity:5,fontFamily:"courier-new, courier, monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:"info",logger:null,scrollback:1e3,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},rightClickSelectsWord:n.isMac,windowOptions:{},windowsMode:!1,windowsPty:{},wordSeparator:" ()[]{}',\"`",altClickMovesCursor:!0,convertEol:!1,termName:"xterm",cancelEvents:!1,overviewRulerWidth:0};const o=["normal","bold","100","200","300","400","500","600","700","800","900"];class a extends r.Disposable{constructor(e){super(),this._onOptionChange=this.register(new s.EventEmitter),this.onOptionChange=this._onOptionChange.event;const i=Object.assign({},t.DEFAULT_OPTIONS);for(const t in e)if(t in i)try{const s=e[t];i[t]=this._sanitizeAndValidateOption(t,s)}catch(e){console.error(e)}this.rawOptions=i,this.options=Object.assign({},i),this._setupOptions()}onSpecificOptionChange(e,t){return this.onOptionChange((i=>{i===e&&t(this.rawOptions[e])}))}onMultipleOptionChange(e,t){return this.onOptionChange((i=>{-1!==e.indexOf(i)&&t()}))}_setupOptions(){const e=e=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key "${e}"`);return this.rawOptions[e]},i=(e,i)=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key "${e}"`);i=this._sanitizeAndValidateOption(e,i),this.rawOptions[e]!==i&&(this.rawOptions[e]=i,this._onOptionChange.fire(e))};for(const t in this.rawOptions){const s={get:e.bind(this,t),set:i.bind(this,t)};Object.defineProperty(this.options,t,s)}}_sanitizeAndValidateOption(e,i){switch(e){case"cursorStyle":if(i||(i=t.DEFAULT_OPTIONS[e]),!function(e){return"block"===e||"underline"===e||"bar"===e}(i))throw new Error(`"${i}" is not a valid value for ${e}`);break;case"wordSeparator":i||(i=t.DEFAULT_OPTIONS[e]);break;case"fontWeight":case"fontWeightBold":if("number"==typeof i&&1<=i&&i<=1e3)break;i=o.includes(i)?i:t.DEFAULT_OPTIONS[e];break;case"cursorWidth":i=Math.floor(i);case"lineHeight":case"tabStopWidth":if(i<1)throw new Error(`${e} cannot be less than 1, value: ${i}`);break;case"minimumContrastRatio":i=Math.max(1,Math.min(21,Math.round(10*i)/10));break;case"scrollback":if((i=Math.min(i,4294967295))<0)throw new Error(`${e} cannot be less than 0, value: ${i}`);break;case"fastScrollSensitivity":case"scrollSensitivity":if(i<=0)throw new Error(`${e} cannot be less than or equal to 0, value: ${i}`);break;case"rows":case"cols":if(!i&&0!==i)throw new Error(`${e} must be numeric, value: ${i}`);break;case"windowsPty":i=null!=i?i:{}}return i}}t.OptionsService=a},2660:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.OscLinkService=void 0;const n=i(2585);let o=t.OscLinkService=class{constructor(e){this._bufferService=e,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(e){const t=this._bufferService.buffer;if(void 0===e.id){const i=t.addMarker(t.ybase+t.y),s={data:e,id:this._nextId++,lines:[i]};return i.onDispose((()=>this._removeMarkerFromLink(s,i))),this._dataByLinkId.set(s.id,s),s.id}const i=e,s=this._getEntryIdKey(i),r=this._entriesWithId.get(s);if(r)return this.addLineToLink(r.id,t.ybase+t.y),r.id;const n=t.addMarker(t.ybase+t.y),o={id:this._nextId++,key:this._getEntryIdKey(i),data:i,lines:[n]};return n.onDispose((()=>this._removeMarkerFromLink(o,n))),this._entriesWithId.set(o.key,o),this._dataByLinkId.set(o.id,o),o.id}addLineToLink(e,t){const i=this._dataByLinkId.get(e);if(i&&i.lines.every((e=>e.line!==t))){const e=this._bufferService.buffer.addMarker(t);i.lines.push(e),e.onDispose((()=>this._removeMarkerFromLink(i,e)))}}getLinkData(e){var t;return null===(t=this._dataByLinkId.get(e))||void 0===t?void 0:t.data}_getEntryIdKey(e){return`${e.id};;${e.uri}`}_removeMarkerFromLink(e,t){const i=e.lines.indexOf(t);-1!==i&&(e.lines.splice(i,1),0===e.lines.length&&(void 0!==e.data.id&&this._entriesWithId.delete(e.key),this._dataByLinkId.delete(e.id)))}};t.OscLinkService=o=s([r(0,n.IBufferService)],o)},8343:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.createDecorator=t.getServiceDependencies=t.serviceRegistry=void 0;const i="di$target",s="di$dependencies";t.serviceRegistry=new Map,t.getServiceDependencies=function(e){return e[s]||[]},t.createDecorator=function(e){if(t.serviceRegistry.has(e))return t.serviceRegistry.get(e);const r=function(e,t,n){if(3!==arguments.length)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");!function(e,t,r){t[i]===t?t[s].push({id:e,index:r}):(t[s]=[{id:e,index:r}],t[i]=t)}(r,e,n)};return r.toString=()=>e,t.serviceRegistry.set(e,r),r}},2585:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.IDecorationService=t.IUnicodeService=t.IOscLinkService=t.IOptionsService=t.ILogService=t.LogLevelEnum=t.IInstantiationService=t.ICharsetService=t.ICoreService=t.ICoreMouseService=t.IBufferService=void 0;const s=i(8343);var r;t.IBufferService=(0,s.createDecorator)("BufferService"),t.ICoreMouseService=(0,s.createDecorator)("CoreMouseService"),t.ICoreService=(0,s.createDecorator)("CoreService"),t.ICharsetService=(0,s.createDecorator)("CharsetService"),t.IInstantiationService=(0,s.createDecorator)("InstantiationService"),function(e){e[e.TRACE=0]="TRACE",e[e.DEBUG=1]="DEBUG",e[e.INFO=2]="INFO",e[e.WARN=3]="WARN",e[e.ERROR=4]="ERROR",e[e.OFF=5]="OFF"}(r||(t.LogLevelEnum=r={})),t.ILogService=(0,s.createDecorator)("LogService"),t.IOptionsService=(0,s.createDecorator)("OptionsService"),t.IOscLinkService=(0,s.createDecorator)("OscLinkService"),t.IUnicodeService=(0,s.createDecorator)("UnicodeService"),t.IDecorationService=(0,s.createDecorator)("DecorationService")},1480:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;const s=i(8460),r=i(225);t.UnicodeService=class{constructor(){this._providers=Object.create(null),this._active="",this._onChange=new s.EventEmitter,this.onChange=this._onChange.event;const e=new r.UnicodeV6;this.register(e),this._active=e.version,this._activeProvider=e}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw new Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(e){let t=0;const i=e.length;for(let s=0;s=i)return t+this.wcwidth(r);const n=e.charCodeAt(s);56320<=n&&n<=57343?r=1024*(r-55296)+n-56320+65536:t+=this.wcwidth(n)}t+=this.wcwidth(r)}return t}}}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var n=t[s]={exports:{}};return e[s].call(n.exports,n,n.exports,i),n.exports}var s={};return(()=>{var e=s;Object.defineProperty(e,"__esModule",{value:!0}),e.Terminal=void 0;const t=i(9042),r=i(3236),n=i(844),o=i(5741),a=i(8285),h=i(7975),c=i(7090),l=["cols","rows"];class d extends n.Disposable{constructor(e){super(),this._core=this.register(new r.Terminal(e)),this._addonManager=this.register(new o.AddonManager),this._publicOptions=Object.assign({},this._core.options);const t=e=>this._core.options[e],i=(e,t)=>{this._checkReadonlyOptions(e),this._core.options[e]=t};for(const e in this._core.options){const s={get:t.bind(this,e),set:i.bind(this,e)};Object.defineProperty(this._publicOptions,e,s)}}_checkReadonlyOptions(e){if(l.includes(e))throw new Error(`Option "${e}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.rawOptions.allowProposedApi)throw new Error("You must set the allowProposedApi option to true to use proposed API")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onKey(){return this._core.onKey}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onSelectionChange(){return this._core.onSelectionChange}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get element(){return this._core.element}get parser(){return this._parser||(this._parser=new h.ParserApi(this._core)),this._parser}get unicode(){return this._checkProposedApi(),new c.UnicodeApi(this._core)}get textarea(){return this._core.textarea}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer||(this._buffer=this.register(new a.BufferNamespaceApi(this._core))),this._buffer}get markers(){return this._checkProposedApi(),this._core.markers}get modes(){const e=this._core.coreService.decPrivateModes;let t="none";switch(this._core.coreMouseService.activeProtocol){case"X10":t="x10";break;case"VT200":t="vt200";break;case"DRAG":t="drag";break;case"ANY":t="any"}return{applicationCursorKeysMode:e.applicationCursorKeys,applicationKeypadMode:e.applicationKeypad,bracketedPasteMode:e.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:t,originMode:e.origin,reverseWraparoundMode:e.reverseWraparound,sendFocusMode:e.sendFocus,wraparoundMode:e.wraparound}}get options(){return this._publicOptions}set options(e){for(const t in e)this._publicOptions[t]=e[t]}blur(){this._core.blur()}focus(){this._core.focus()}resize(e,t){this._verifyIntegers(e,t),this._core.resize(e,t)}open(e){this._core.open(e)}attachCustomKeyEventHandler(e){this._core.attachCustomKeyEventHandler(e)}registerLinkProvider(e){return this._core.registerLinkProvider(e)}registerCharacterJoiner(e){return this._checkProposedApi(),this._core.registerCharacterJoiner(e)}deregisterCharacterJoiner(e){this._checkProposedApi(),this._core.deregisterCharacterJoiner(e)}registerMarker(e=0){return this._verifyIntegers(e),this._core.registerMarker(e)}registerDecoration(e){var t,i,s;return this._checkProposedApi(),this._verifyPositiveIntegers(null!==(t=e.x)&&void 0!==t?t:0,null!==(i=e.width)&&void 0!==i?i:0,null!==(s=e.height)&&void 0!==s?s:0),this._core.registerDecoration(e)}hasSelection(){return this._core.hasSelection()}select(e,t,i){this._verifyIntegers(e,t,i),this._core.select(e,t,i)}getSelection(){return this._core.getSelection()}getSelectionPosition(){return this._core.getSelectionPosition()}clearSelection(){this._core.clearSelection()}selectAll(){this._core.selectAll()}selectLines(e,t){this._verifyIntegers(e,t),this._core.selectLines(e,t)}dispose(){super.dispose()}scrollLines(e){this._verifyIntegers(e),this._core.scrollLines(e)}scrollPages(e){this._verifyIntegers(e),this._core.scrollPages(e)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(e){this._verifyIntegers(e),this._core.scrollToLine(e)}clear(){this._core.clear()}write(e,t){this._core.write(e,t)}writeln(e,t){this._core.write(e),this._core.write("\r\n",t)}paste(e){this._core.paste(e)}refresh(e,t){this._verifyIntegers(e,t),this._core.refresh(e,t)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(e){this._addonManager.loadAddon(this,e)}static get strings(){return t}_verifyIntegers(...e){for(const t of e)if(t===1/0||isNaN(t)||t%1!=0)throw new Error("This API only accepts integers")}_verifyPositiveIntegers(...e){for(const t of e)if(t&&(t===1/0||isNaN(t)||t%1!=0||t<0))throw new Error("This API only accepts positive integers")}}e.Terminal=d})(),s})())); +//# sourceMappingURL=xterm.js.map \ No newline at end of file diff --git a/server/go/web/embed.go b/server/go/web/embed.go new file mode 100644 index 0000000..bef3d60 --- /dev/null +++ b/server/go/web/embed.go @@ -0,0 +1,23 @@ +package web + +import _ "embed" + +// IndexHTML is the web remote desktop landing page, synced from +// server/web/index.html via `make sync` (or VSCode's sync-assets task). +// Do not edit assets/index.html directly — source of truth lives at +// server/web/index.html. +// +//go:embed assets/index.html +var IndexHTML []byte + +// Third-party xterm.js library assets. Checked in as-is; updates are +// infrequent and done manually from server/2015Remote/res/web/. + +//go:embed assets/static/xterm.min.js +var xtermJS []byte + +//go:embed assets/static/xterm.css +var xtermCSS []byte + +//go:embed assets/static/fit.min.js +var xtermFitJS []byte diff --git a/server/go/web/server.go b/server/go/web/server.go new file mode 100644 index 0000000..1cdfc0b --- /dev/null +++ b/server/go/web/server.go @@ -0,0 +1,119 @@ +package web + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "strconv" + "time" + + "github.com/yuanyuanxiang/SimpleRemoter/server/go/logger" +) + +// Server serves the web remote desktop UI: the embedded index.html, xterm.js +// static assets, and the PWA manifest. WebSocket signaling, device list and +// screen streaming will be wired up in later phases. +type Server struct { + port int + log *logger.Logger + srv *http.Server +} + +// New creates an HTTP server bound to the given port. port=0 disables the server. +func New(port int, log *logger.Logger) *Server { + return &Server{port: port, log: log} +} + +// Start launches the server in a goroutine and returns immediately. +// If port is 0, returns nil without starting anything. +func (s *Server) Start() error { + if s.port == 0 { + s.log.Info("HTTP server disabled (-http-port=0)") + return nil + } + + mux := http.NewServeMux() + mux.HandleFunc("/", s.handleIndex) + mux.HandleFunc("/health", s.handleHealth) + mux.HandleFunc("/manifest.json", s.handleManifest) + mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8")) + mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8")) + mux.HandleFunc("/static/xterm-fit.js", staticHandler(xtermFitJS, "application/javascript; charset=utf-8")) + + s.srv = &http.Server{ + Addr: ":" + strconv.Itoa(s.port), + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + } + + // Bind synchronously so port-in-use / permission errors propagate to the + // caller instead of being lost inside the goroutine after a misleading + // "started" log line. + ln, err := net.Listen("tcp", s.srv.Addr) + if err != nil { + return fmt.Errorf("listen on :%d: %w", s.port, err) + } + s.log.Info("HTTP server started on :%d", s.port) + go func() { + if err := s.srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.log.Error("HTTP server stopped: %v", err) + } + }() + return nil +} + +// Stop gracefully shuts the server down. +func (s *Server) Stop() { + if s.srv == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = s.srv.Shutdown(ctx) +} + +func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" && r.URL.Path != "/index.html" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + _, _ = w.Write(IndexHTML) +} + +func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"ok"}`)) +} + +// PWA manifest. Referenced by in index.html. +// Static JSON, no template needed. +const manifestJSON = `{ + "name": "SimpleRemoter", + "short_name": "Remoter", + "start_url": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#1a1a2e", + "theme_color": "#1a1a2e" +}` + +func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/manifest+json") + w.Header().Set("Cache-Control", "public, max-age=86400") + _, _ = w.Write([]byte(manifestJSON)) +} + +// staticHandler returns an http.HandlerFunc that serves a fixed byte slice +// with the given content-type. Used for embedded third-party assets (xterm.js etc.) +// that change infrequently — 1-day browser cache is fine. +func staticHandler(body []byte, contentType string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "public, max-age=86400") + _, _ = w.Write(body) + } +} -- 2.43.0 From b1f229706c32ff47cf4c41fe802625d37e520108 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 17 May 2026 22:18:29 +0200 Subject: [PATCH 3/7] Feature(Go): Web auth, WebSocket signaling and live device list (Phase 3) --- server/go/.vscode/launch.json | 12 +- server/go/README.md | 13 +- server/go/cmd/main.go | 83 +++++++++++- server/go/go.mod | 1 + server/go/go.sum | 2 + server/go/hub/hub.go | 217 +++++++++++++++++++++++++++++++ server/go/hub/hub_test.go | 152 ++++++++++++++++++++++ server/go/protocol/commands.go | 21 +-- server/go/web/server.go | 35 ++++- server/go/web/ws.go | 222 ++++++++++++++++++++++++++++++++ server/go/web/ws_handlers.go | 166 ++++++++++++++++++++++++ server/go/wsauth/wsauth.go | 192 +++++++++++++++++++++++++++ server/go/wsauth/wsauth_test.go | 116 +++++++++++++++++ 13 files changed, 1211 insertions(+), 21 deletions(-) create mode 100644 server/go/hub/hub.go create mode 100644 server/go/hub/hub_test.go create mode 100644 server/go/web/ws.go create mode 100644 server/go/web/ws_handlers.go create mode 100644 server/go/wsauth/wsauth.go create mode 100644 server/go/wsauth/wsauth_test.go diff --git a/server/go/.vscode/launch.json b/server/go/.vscode/launch.json index 30d1af3..8eb08a4 100644 --- a/server/go/.vscode/launch.json +++ b/server/go/.vscode/launch.json @@ -8,8 +8,12 @@ "mode": "auto", "program": "${workspaceFolder}/cmd", "cwd": "${workspaceFolder}", - "args": [], - "env": {}, + "args": [ + "-port=9090" + ], + "env": { + "YAMA_WEB_ADMIN_PASS": "3.14159" + }, "console": "integratedTerminal", "preLaunchTask": "sync-web-assets" }, @@ -23,7 +27,9 @@ "args": [ "-port=9090" ], - "env": {}, + "env": { + "YAMA_WEB_ADMIN_PASS": "3.14159" + }, "console": "integratedTerminal", "buildFlags": "-gcflags='all=-N -l'", "preLaunchTask": "sync-web-assets" diff --git a/server/go/README.md b/server/go/README.md index 65c50c7..25a4c7e 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -25,9 +25,15 @@ server/go/ │ └── pool.go # Goroutine 工作池 ├── logger/ │ └── logger.go # 日志模块 (基于 zerolog) +├── hub/ +│ └── hub.go # 在线设备注册表 + 事件订阅 +├── wsauth/ +│ └── wsauth.go # Web 鉴权 (challenge-response + 不透明 token) ├── web/ │ ├── embed.go # //go:embed 嵌入 HTML/xterm.js 等 web 资源 -│ ├── server.go # HTTP server (静态页面 + 后续 WS 信令) +│ ├── server.go # HTTP server (静态页面 + REST + WS 路由) +│ ├── ws.go # WebSocket 连接生命周期 +│ ├── ws_handlers.go # WS 消息分发与处理 │ └── assets/ │ ├── index.html # 从 ../../web/index.html sync 而来 (gitignored) │ └── static/ # 第三方 xterm.js 资源 (checked in) @@ -109,9 +115,10 @@ VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。 ### 环境变量 | 变量 | 说明 | 示例 | -|------|------|------| +| ---- | ---- | ---- | | `YAMA_PWDHASH` | 密码的 SHA256 哈希值 (64位十六进制) | `61f04dd6...` | -| `YAMA_PWD` | 超级密码,用于 HMAC 签名验证 | `your_super_password` | +| `YAMA_PWD` | 超级密码,用于 HMAC 签名验证;也作为 Web admin 密码的默认来源 | `your_super_password` | +| `YAMA_WEB_ADMIN_PASS` | Web UI 的 admin 密码(明文);优先于 `YAMA_PWD`。两者都未设置时 Web 登录禁用 | `your_admin_password` | ```bash # Linux/macOS diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index d6da965..111e91e 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -8,13 +8,16 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/yuanyuanxiang/SimpleRemoter/server/go/auth" "github.com/yuanyuanxiang/SimpleRemoter/server/go/connection" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/hub" "github.com/yuanyuanxiang/SimpleRemoter/server/go/logger" "github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol" "github.com/yuanyuanxiang/SimpleRemoter/server/go/server" "github.com/yuanyuanxiang/SimpleRemoter/server/go/web" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/wsauth" ) // MyHandler implements the server.Handler interface @@ -22,6 +25,7 @@ type MyHandler struct { log *logger.Logger auth *auth.Authenticator srv *server.Server + hub *hub.Hub } // OnConnect is called when a client connects @@ -37,6 +41,7 @@ func (h *MyHandler) OnDisconnect(ctx *connection.Context) { "clientID", info.ClientID, "computer", info.ComputerName, ) + h.hub.Unregister(info.ClientID) } } @@ -110,8 +115,40 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { "version", info.ModuleVersion, "path", clientInfo.FilePath, ) + + // PCName carries "ComputerName/Group"; ModuleVersion carries "Version-Capability". + // strings.Cut returns the full string as the head when the separator is + // absent, which gives us the natural "no group / no capability" fallback. + name, group, _ := strings.Cut(info.PCName, "/") + version, capability, _ := strings.Cut(info.ModuleVersion, "-") + + // Reserved field 10 (ClientLoc) is the client-reported geo string. + location := "" + if len(reserved) > 10 { + location = info.GetReservedField(10) + } + + // Register with hub so the web side can list this device. Sub-connections + // (screen / terminal etc.) reuse the MasterID and will overwrite this entry + // harmlessly, but only the main login carries enough info to be useful here. + h.hub.Register(&hub.Device{ + ID: clientID, + Name: name, + Group: group, + Version: version, + Capability: capability, + OS: info.OsVerInfo, + CPU: clientInfo.CPU, + FilePath: clientInfo.FilePath, + InstallTime: info.StartTime, + Location: location, + PeerIP: ctx.GetPeerIP(), + PublicIP: clientInfo.IP, + ConnectedAt: time.Now(), + }) } + // handleAuth handles authorization request (TOKEN_AUTH = 100) func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) { result := h.auth.Authenticate(data) @@ -160,6 +197,25 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) { uint64(data[5])<<32 | uint64(data[6])<<40 | uint64(data[7])<<48 | uint64(data[8])<<56 } + // Forward live fields (ActiveWnd + Ping) to the hub so the web UI can + // display current latency and foreground window per device. Skip until + // login has happened — the hub is keyed by MasterID, which only exists + // post-login. + if info := ctx.GetInfo(); info.ClientID != "" { + var rtt int32 + var activeWindow string + // ActiveWnd at data[9..521] is a 512-byte GBK-encoded string. + if len(data) >= 9+512 { + activeWindow = protocol.GbkToUTF8(data[9 : 9+512]) + } + // Ping at data[521..525] is a little-endian int32. + if len(data) >= 525 { + rtt = int32(uint32(data[521]) | uint32(data[522])<<8 | + uint32(data[523])<<16 | uint32(data[524])<<24) + } + h.hub.UpdateLive(info.ClientID, int(rtt), activeWindow) + } + // Authenticate heartbeat if it contains authorization info // data[1:] skips the command byte to get the raw Heartbeat structure var authorized byte = 0 @@ -269,6 +325,27 @@ func main() { // Create authenticator (shared by all servers) authenticator := auth.New(authCfg) + // Shared device registry — every TCP handler reports devices into it, + // the HTTP server reads from it. + deviceHub := hub.New() + + // Web user authenticator. Bootstrap admin from env var YAMA_WEB_ADMIN_PASS; + // if unset, fall back to YAMA_PWD (same secret the TCP authorization uses) + // so a single password env var is enough to bring up the whole stack. + // If neither is set, no admin is registered and login will always fail — + // the user must define a password before browsers can log in. + webAuth := wsauth.New() + adminPass := os.Getenv("YAMA_WEB_ADMIN_PASS") + if adminPass == "" { + adminPass = os.Getenv("YAMA_PWD") + } + if adminPass != "" { + webAuth.AddAdminFromPlainPassword("admin", adminPass) + log.Info("Web admin user configured") + } else { + log.Warn("Neither YAMA_WEB_ADMIN_PASS nor YAMA_PWD is set; web login will be unavailable") + } + // Create servers for each port var servers []*server.Server for _, port := range ports { @@ -284,6 +361,7 @@ func main() { log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)), auth: authenticator, srv: srv, + hub: deviceHub, } srv.SetHandler(handler) @@ -297,8 +375,9 @@ func main() { } } - // Start HTTP server for web UI (Phase 1: serves index.html only) - httpSrv := web.New(*httpPort, log.WithPrefix("Web")) + // Start HTTP server for web UI. Hub gives it read-only access to the + // device registry; the authenticator owns user accounts and session tokens. + httpSrv := web.New(*httpPort, log.WithPrefix("Web"), deviceHub, webAuth) if err := httpSrv.Start(); err != nil { log.Fatal("Failed to start HTTP server: %v", err) } diff --git a/server/go/go.mod b/server/go/go.mod index a1d5f04..6cb5c13 100644 --- a/server/go/go.mod +++ b/server/go/go.mod @@ -3,6 +3,7 @@ module github.com/yuanyuanxiang/SimpleRemoter/server/go go 1.24.5 require ( + github.com/gorilla/websocket v1.5.3 github.com/klauspost/compress v1.18.2 github.com/rs/zerolog v1.34.0 golang.org/x/text v0.32.0 diff --git a/server/go/go.sum b/server/go/go.sum index d2f3d60..bab1d66 100644 --- a/server/go/go.sum +++ b/server/go/go.sum @@ -1,5 +1,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/server/go/hub/hub.go b/server/go/hub/hub.go new file mode 100644 index 0000000..86365b8 --- /dev/null +++ b/server/go/hub/hub.go @@ -0,0 +1,217 @@ +// Package hub maintains the registry of currently online devices and acts as +// the bridge between the TCP server (which sees raw client connections) and +// the web server (which serves browser clients). +// +// The TCP side calls RegisterDevice / UnregisterDevice as clients come and go. +// The web side calls ListDevices / GetDevice / (Phase 4) SendToDevice. +// Neither side imports the other — both depend only on this package. +// +// Phase 3 scope: device list only. Frame/cursor pub-sub and SendToDevice are +// added in later phases as features need them. +package hub + +import ( + "sync" + "time" + + "github.com/yuanyuanxiang/SimpleRemoter/server/go/connection" +) + +// Device is the internal record for one logical end-device (keyed by MasterID). +// A single device may use multiple TCP sub-connections (screen, terminal …); +// only the main login connection is stored here. +// +// PCName from LOGIN_INFOR is interpreted as "ComputerName/Group" and +// ModuleVersion as "Version-Capability"; the split halves live in separate +// fields so the front-end can render them independently. +type Device struct { + ID string // MasterID — stable identifier the client reports at login + Name string // PCName before '/' (real computer name) + Group string // PCName after '/' (group label; may be empty) + Version string // ModuleVersion before '-' (semantic version) + Capability string // ModuleVersion after '-' (capability tags; may be empty) + OS string // OS version string + CPU string // from LOGIN_INFOR reserved field 2 + FilePath string // from LOGIN_INFOR reserved field 4 + InstallTime string // from LOGIN_INFOR reserved field 6 (or StartTime) + Location string // client-reported geo string (reserved field 10) + PeerIP string // network-level remote address as seen by the server + PublicIP string // client-reported public IP (reserved field 11) + ConnectedAt time.Time + + // Live fields refreshed on every heartbeat. Protected by hub.mu. + RTT int // network latency in ms (Heartbeat.Ping) + ActiveWindow string // foreground window title (Heartbeat.ActiveWnd, decoded) + + // conn is the main connection's context. Web side will use it in Phase 4 + // to push COMMAND_SCREEN_SPY and similar commands via the hub. + conn *connection.Context +} + +// DeviceInfo is the JSON-safe projection of Device for the /api/devices +// endpoint and the WS device_list message. Field names match what the +// existing browser front-end expects. +type DeviceInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Group string `json:"group,omitempty"` + Version string `json:"version"` + Capability string `json:"capability,omitempty"` + OS string `json:"os"` + CPU string `json:"cpu,omitempty"` + FilePath string `json:"file_path,omitempty"` + InstallTime string `json:"install_time,omitempty"` + Location string `json:"location,omitempty"` + IP string `json:"ip"` // client-reported public IP (matches C++ key) + PeerIP string `json:"peer_ip,omitempty"` + RTT int `json:"rtt"` + ActiveWindow string `json:"activeWindow,omitempty"` + ConnectedAt int64 `json:"connected_at"` + Online bool `json:"online"` +} + +// EventHandler receives notifications about device lifecycle and per-tick +// live updates. Methods are invoked synchronously from Register / Unregister / +// UpdateLive — implementations must be non-blocking (typically just write to +// a channel or queue). +type EventHandler interface { + OnDeviceOnline(d DeviceInfo) + OnDeviceOffline(id string) + OnDeviceUpdate(id string, rtt int, activeWindow string) +} + +// Hub is a thread-safe registry of online devices. +type Hub struct { + mu sync.RWMutex + devices map[string]*Device + subMu sync.RWMutex + subscribers []EventHandler +} + +// New returns an empty Hub. +func New() *Hub { + return &Hub{devices: make(map[string]*Device)} +} + +// Subscribe registers an EventHandler. The returned func removes it. +// Multiple handlers are supported; each receives every event. +func (h *Hub) Subscribe(eh EventHandler) (unsubscribe func()) { + h.subMu.Lock() + h.subscribers = append(h.subscribers, eh) + h.subMu.Unlock() + return func() { + h.subMu.Lock() + defer h.subMu.Unlock() + for i, x := range h.subscribers { + if x == eh { + h.subscribers = append(h.subscribers[:i], h.subscribers[i+1:]...) + return + } + } + } +} + +func (h *Hub) snapshotSubscribers() []EventHandler { + h.subMu.RLock() + defer h.subMu.RUnlock() + out := make([]EventHandler, len(h.subscribers)) + copy(out, h.subscribers) + return out +} + +// Register records a device as online. Re-registering an existing ID overwrites +// the previous entry (e.g. a client reconnect with the same MasterID). +// A nil device or empty ID is silently ignored. +// Subscribers are notified after the device is added. +func (h *Hub) Register(d *Device) { + if d == nil || d.ID == "" { + return + } + h.mu.Lock() + h.devices[d.ID] = d + info := deviceToInfo(d) + h.mu.Unlock() + for _, s := range h.snapshotSubscribers() { + s.OnDeviceOnline(info) + } +} + +// Unregister removes a device by ID. No-op if not present. +// Subscribers are notified after the device is removed (only if it existed). +func (h *Hub) Unregister(id string) { + if id == "" { + return + } + h.mu.Lock() + _, existed := h.devices[id] + delete(h.devices, id) + h.mu.Unlock() + if !existed { + return + } + for _, s := range h.snapshotSubscribers() { + s.OnDeviceOffline(id) + } +} + +// ListDevices returns a fresh snapshot slice. The caller may mutate it freely; +// it shares no state with the hub. +func (h *Hub) ListDevices() []DeviceInfo { + h.mu.RLock() + defer h.mu.RUnlock() + out := make([]DeviceInfo, 0, len(h.devices)) + for _, d := range h.devices { + out = append(out, deviceToInfo(d)) + } + return out +} + +func deviceToInfo(d *Device) DeviceInfo { + return DeviceInfo{ + ID: d.ID, + Name: d.Name, + Group: d.Group, + Version: d.Version, + Capability: d.Capability, + OS: d.OS, + CPU: d.CPU, + FilePath: d.FilePath, + InstallTime: d.InstallTime, + Location: d.Location, + IP: d.PublicIP, + PeerIP: d.PeerIP, + RTT: d.RTT, + ActiveWindow: d.ActiveWindow, + ConnectedAt: d.ConnectedAt.Unix(), + Online: true, // a device that's in the map is by definition online + } +} + +// UpdateLive applies a heartbeat-derived RTT and active-window title to the +// device's live fields, then notifies subscribers. No-op if the device is +// not registered (e.g. heartbeat arriving for a connection that never sent +// TOKEN_LOGIN or has already disconnected). +func (h *Hub) UpdateLive(id string, rtt int, activeWindow string) { + if id == "" { + return + } + h.mu.Lock() + d, ok := h.devices[id] + if !ok { + h.mu.Unlock() + return + } + d.RTT = rtt + d.ActiveWindow = activeWindow + h.mu.Unlock() + for _, s := range h.snapshotSubscribers() { + s.OnDeviceUpdate(id, rtt, activeWindow) + } +} + +// Count returns the current number of online devices. +func (h *Hub) Count() int { + h.mu.RLock() + defer h.mu.RUnlock() + return len(h.devices) +} diff --git a/server/go/hub/hub_test.go b/server/go/hub/hub_test.go new file mode 100644 index 0000000..930cc4f --- /dev/null +++ b/server/go/hub/hub_test.go @@ -0,0 +1,152 @@ +package hub + +import ( + "fmt" + "sync" + "testing" + "time" +) + +func TestHubRegisterListUnregister(t *testing.T) { + h := New() + if got := h.Count(); got != 0 { + t.Fatalf("empty hub: want Count=0, got %d", got) + } + + h.Register(&Device{ID: "a", Name: "Alice", ConnectedAt: time.Now()}) + h.Register(&Device{ID: "b", Name: "Bob", ConnectedAt: time.Now()}) + if got := h.Count(); got != 2 { + t.Fatalf("after 2 registers: want Count=2, got %d", got) + } + + list := h.ListDevices() + if len(list) != 2 { + t.Fatalf("want 2 devices in list, got %d", len(list)) + } + + h.Unregister("a") + if got := h.Count(); got != 1 { + t.Fatalf("after unregister: want Count=1, got %d", got) + } + + // Unregister non-existent ID is a no-op + h.Unregister("ghost") + if got := h.Count(); got != 1 { + t.Fatalf("after no-op unregister: want Count=1, got %d", got) + } +} + +func TestHubNilAndEmptyIgnored(t *testing.T) { + h := New() + h.Register(nil) + h.Register(&Device{ID: ""}) + h.Unregister("") + if got := h.Count(); got != 0 { + t.Fatalf("nil/empty register should be no-op, got Count=%d", got) + } +} + +type captureHandler struct { + mu sync.Mutex + online []string + offline []string + updates []string // formatted "id:rtt" +} + +func (c *captureHandler) OnDeviceOnline(d DeviceInfo) { + c.mu.Lock() + c.online = append(c.online, d.ID) + c.mu.Unlock() +} + +func (c *captureHandler) OnDeviceOffline(id string) { + c.mu.Lock() + c.offline = append(c.offline, id) + c.mu.Unlock() +} + +func (c *captureHandler) OnDeviceUpdate(id string, rtt int, _ string) { + c.mu.Lock() + c.updates = append(c.updates, fmt.Sprintf("%s:%d", id, rtt)) + c.mu.Unlock() +} + +func TestHubSubscribeEvents(t *testing.T) { + h := New() + c := &captureHandler{} + unsub := h.Subscribe(c) + + h.Register(&Device{ID: "x", Name: "x"}) + h.Register(&Device{ID: "y", Name: "y"}) + h.Unregister("x") + h.Unregister("nonexistent") // no event + + if len(c.online) != 2 || c.online[0] != "x" || c.online[1] != "y" { + t.Fatalf("online events: %+v", c.online) + } + if len(c.offline) != 1 || c.offline[0] != "x" { + t.Fatalf("offline events: %+v", c.offline) + } + + unsub() + h.Register(&Device{ID: "z"}) + if len(c.online) != 2 { + t.Fatalf("after unsubscribe should not receive events: %+v", c.online) + } +} + +func TestHubUpdateLive(t *testing.T) { + h := New() + c := &captureHandler{} + h.Subscribe(c) + + h.Register(&Device{ID: "x", Name: "x"}) + h.UpdateLive("x", 42, "Notepad") + h.UpdateLive("ghost", 999, "should be ignored") // unknown id, no event + + if len(c.updates) != 1 || c.updates[0] != "x:42" { + t.Fatalf("updates: %+v", c.updates) + } + + list := h.ListDevices() + if list[0].RTT != 42 || list[0].ActiveWindow != "Notepad" { + t.Fatalf("live fields not applied: %+v", list[0]) + } +} + +func TestHubRegisterOverwrites(t *testing.T) { + h := New() + h.Register(&Device{ID: "x", Name: "first"}) + h.Register(&Device{ID: "x", Name: "second"}) + list := h.ListDevices() + if len(list) != 1 || list[0].Name != "second" { + t.Fatalf("re-register should overwrite, got %+v", list) + } +} + +// Race detector should not fire under `go test -race ./hub/...`. +func TestHubConcurrent(t *testing.T) { + h := New() + const goroutines = 50 + const opsPer = 100 + + var wg sync.WaitGroup + for g := range goroutines { + wg.Add(1) + go func(g int) { + defer wg.Done() + for i := range opsPer { + id := fmt.Sprintf("g%d-%d", g, i) + h.Register(&Device{ID: id, Name: id, ConnectedAt: time.Now()}) + _ = h.ListDevices() + _ = h.Count() + h.Unregister(id) + } + }(g) + } + wg.Wait() + + if got := h.Count(); got != 0 { + t.Fatalf("after all unregisters: want 0, got %d", got) + } +} diff --git a/server/go/protocol/commands.go b/server/go/protocol/commands.go index 320eb3f..b5ea1c8 100644 --- a/server/go/protocol/commands.go +++ b/server/go/protocol/commands.go @@ -9,8 +9,11 @@ import ( "golang.org/x/text/transform" ) -// gbkToUTF8 converts GBK encoded bytes to UTF-8 string -func gbkToUTF8(data []byte) string { +// GbkToUTF8 converts GBK encoded bytes to UTF-8 string. The input is treated +// as a null-terminated GBK buffer (typical for Windows clients); content +// after the first NUL byte is discarded. Non-printable characters are +// stripped from the result. +func GbkToUTF8(data []byte) string { // Find the first null byte and truncate there if idx := bytes.IndexByte(data, 0); idx >= 0 { data = data[:idx] @@ -111,17 +114,17 @@ func ParseLoginInfo(data []byte) (*LoginInfo, error) { // Parse module version (offset 164, 24 bytes) // This contains date string like "Dec 19 2025" if len(data) >= OffsetModuleVersion+24 { - info.ModuleVersion = gbkToUTF8(data[OffsetModuleVersion : OffsetModuleVersion+24]) + info.ModuleVersion = GbkToUTF8(data[OffsetModuleVersion : OffsetModuleVersion+24]) } // Parse PC name (offset 188, 240 bytes) if len(data) >= OffsetPCName+240 { - info.PCName = gbkToUTF8(data[OffsetPCName : OffsetPCName+240]) + info.PCName = GbkToUTF8(data[OffsetPCName : OffsetPCName+240]) } // Parse Master ID (offset 428, 20 bytes) if len(data) >= OffsetMasterID+20 { - info.MasterID = gbkToUTF8(data[OffsetMasterID : OffsetMasterID+20]) + info.MasterID = GbkToUTF8(data[OffsetMasterID : OffsetMasterID+20]) } // Parse WebCam exist (offset 448, 4 bytes) @@ -136,14 +139,14 @@ func ParseLoginInfo(data []byte) (*LoginInfo, error) { // Parse Start time (offset 456, 20 bytes) if len(data) >= OffsetStartTime+20 { - info.StartTime = gbkToUTF8(data[OffsetStartTime : OffsetStartTime+20]) + info.StartTime = GbkToUTF8(data[OffsetStartTime : OffsetStartTime+20]) } // Parse Reserved (offset 476, 512 bytes) - contains additional info if len(data) >= OffsetReserved+512 { - info.Reserved = gbkToUTF8(data[OffsetReserved : OffsetReserved+512]) + info.Reserved = GbkToUTF8(data[OffsetReserved : OffsetReserved+512]) } else if len(data) > OffsetReserved { - info.Reserved = gbkToUTF8(data[OffsetReserved:]) + info.Reserved = GbkToUTF8(data[OffsetReserved:]) } return info, nil @@ -152,7 +155,7 @@ func ParseLoginInfo(data []byte) (*LoginInfo, error) { // parseOsVersionInfo parses the OS version info field // The C++ client fills this with a readable string like "Windows 10" via getSystemName() func parseOsVersionInfo(data []byte) string { - return gbkToUTF8(data) + return GbkToUTF8(data) } // ParseReserved parses the reserved field into a slice of strings diff --git a/server/go/web/server.go b/server/go/web/server.go index 1cdfc0b..565de0e 100644 --- a/server/go/web/server.go +++ b/server/go/web/server.go @@ -2,6 +2,7 @@ package web import ( "context" + "encoding/json" "errors" "fmt" "net" @@ -9,21 +10,28 @@ import ( "strconv" "time" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/hub" "github.com/yuanyuanxiang/SimpleRemoter/server/go/logger" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/wsauth" ) // Server serves the web remote desktop UI: the embedded index.html, xterm.js -// static assets, and the PWA manifest. WebSocket signaling, device list and -// screen streaming will be wired up in later phases. +// static assets, the PWA manifest, and JSON APIs backed by the device hub. +// WebSocket signaling and screen streaming will be wired up in later phases. type Server struct { port int log *logger.Logger srv *http.Server + hub *hub.Hub + auth *wsauth.Authenticator + ws *wsHub } // New creates an HTTP server bound to the given port. port=0 disables the server. -func New(port int, log *logger.Logger) *Server { - return &Server{port: port, log: log} +// The hub provides read access to the online-device registry; the authenticator +// owns user accounts and session tokens. +func New(port int, log *logger.Logger, h *hub.Hub, auth *wsauth.Authenticator) *Server { + return &Server{port: port, log: log, hub: h, auth: auth} } // Start launches the server in a goroutine and returns immediately. @@ -34,10 +42,14 @@ func (s *Server) Start() error { return nil } + s.ws = newWSHub(s.auth, s.hub, s.log) + mux := http.NewServeMux() mux.HandleFunc("/", s.handleIndex) mux.HandleFunc("/health", s.handleHealth) mux.HandleFunc("/manifest.json", s.handleManifest) + mux.HandleFunc("/api/devices", s.handleDevices) + mux.HandleFunc("/ws", s.ws.serve) mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8")) mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8")) mux.HandleFunc("/static/xterm-fit.js", staticHandler(xtermFitJS, "application/javascript; charset=utf-8")) @@ -66,6 +78,9 @@ func (s *Server) Start() error { // Stop gracefully shuts the server down. func (s *Server) Stop() { + if s.ws != nil { + s.ws.stop() + } if s.srv == nil { return } @@ -89,6 +104,18 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"status":"ok"}`)) } +// handleDevices returns a JSON snapshot of currently-online devices. Empty +// array (not null) when no clients are connected — matches what the front-end +// will eventually expect. +func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) { + devices := s.hub.ListDevices() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + if err := json.NewEncoder(w).Encode(devices); err != nil { + s.log.Error("encode /api/devices: %v", err) + } +} + // PWA manifest. Referenced by in index.html. // Static JSON, no template needed. const manifestJSON = `{ diff --git a/server/go/web/ws.go b/server/go/web/ws.go new file mode 100644 index 0000000..9af98c2 --- /dev/null +++ b/server/go/web/ws.go @@ -0,0 +1,222 @@ +package web + +import ( + "encoding/json" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/hub" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/logger" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/wsauth" +) + +// ----- WS framing knobs --------------------------------------------------- + +const ( + wsWriteWait = 10 * time.Second // single-frame write deadline + wsReadLimit = 1 << 20 // refuse incoming frames over 1 MB + wsSendBuffer = 64 // outbound queue depth per client +) + +// upgrader allows any origin — this service is meant to be tunneled through +// frp, so requests can legitimately arrive from arbitrary front-end hosts. +// Adjust CheckOrigin once we have a deployment story. +var upgrader = websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { return true }, +} + +// ----- per-connection client state ---------------------------------------- + +type wsClient struct { + conn *websocket.Conn + send chan []byte + closed chan struct{} + once sync.Once + + // Mutated under wsHub.mu (or only by the read loop owning this client). + nonce string // outstanding challenge — cleared after a successful login + token string // set once authenticated + role string // mirrors session role after login + addr string // client address for logs +} + +// queue writes a payload onto the send buffer. Drops silently if the buffer +// is full so a stuck reader can't back-pressure the broadcast path. +func (c *wsClient) queue(payload []byte) { + select { + case c.send <- payload: + case <-c.closed: + default: + // queue full — caller is responsible for noticing if it matters. + } +} + +// close signals both loops to exit. Safe to call multiple times. +func (c *wsClient) close() { + c.once.Do(func() { + close(c.closed) + _ = c.conn.Close() + }) +} + +// ----- ws hub: registry of all connected browsers ------------------------- + +type wsHub struct { + auth *wsauth.Authenticator + devices *hub.Hub + log *logger.Logger + + mu sync.RWMutex + clients map[*wsClient]struct{} + + unsub func() +} + +func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger) *wsHub { + h := &wsHub{ + auth: auth, + devices: devices, + log: log, + clients: make(map[*wsClient]struct{}), + } + h.unsub = devices.Subscribe(h) + return h +} + +// stop unsubscribes from the device hub. Existing connections keep running +// until they close on their own; we only block new event delivery. +func (h *wsHub) stop() { + if h.unsub != nil { + h.unsub() + h.unsub = nil + } +} + +// hub.EventHandler — invoked from hub.Register / hub.Unregister. +func (h *wsHub) OnDeviceOnline(_ hub.DeviceInfo) { + h.broadcastAuthenticated(`{"cmd":"devices_changed"}`) +} + +func (h *wsHub) OnDeviceOffline(_ string) { + h.broadcastAuthenticated(`{"cmd":"devices_changed"}`) +} + +// OnDeviceUpdate forwards heartbeat-derived liveness data so the device-list +// rows can refresh RTT and active-window labels without re-fetching. +func (h *wsHub) OnDeviceUpdate(id string, rtt int, activeWindow string) { + payload := mustJSON(map[string]any{ + "cmd": "device_update", + "id": id, + "rtt": rtt, + "activeWindow": activeWindow, + }) + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.token != "" { + c.queue(payload) + } + } +} + +func (h *wsHub) broadcastAuthenticated(msg string) { + payload := []byte(msg) + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.token != "" { + c.queue(payload) + } + } +} + +func (h *wsHub) register(c *wsClient) { + h.mu.Lock() + h.clients[c] = struct{}{} + h.mu.Unlock() +} + +func (h *wsHub) unregister(c *wsClient) { + h.mu.Lock() + delete(h.clients, c) + h.mu.Unlock() + // Do NOT revoke the token: tokens are session-scoped, not WS-scoped. + // Frontend may close+reopen the WS at any time (visibilitychange handler, + // brief network blip, reload) and must be able to resume with the same + // cached token. The token expires on its own TTL. + c.close() +} + +// ----- HTTP handler ------------------------------------------------------- + +func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + h.log.Error("ws upgrade: %v", err) + return + } + conn.SetReadLimit(wsReadLimit) + + nonce, err := wsauth.NewNonce() + if err != nil { + h.log.Error("nonce gen: %v", err) + _ = conn.Close() + return + } + + client := &wsClient{ + conn: conn, + send: make(chan []byte, wsSendBuffer), + closed: make(chan struct{}), + nonce: nonce, + addr: r.RemoteAddr, + } + h.register(client) + defer h.unregister(client) + + go h.writeLoop(client) + + // Greet with a challenge nonce so the browser can compute the login response. + client.queue([]byte(`{"cmd":"challenge","nonce":"` + nonce + `"}`)) + + h.readLoop(client) +} + +// writeLoop drains the send queue. Exits when the channel is closed or a +// write fails. Closing the underlying connection is the read loop's job. +func (h *wsHub) writeLoop(c *wsClient) { + for { + select { + case msg := <-c.send: + _ = c.conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) + if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { + c.close() + return + } + case <-c.closed: + return + } + } +} + +// readLoop dispatches incoming messages. Exits on read error (peer closed, +// timeout, malformed frame, etc.), which then triggers unregister cleanup. +func (h *wsHub) readLoop(c *wsClient) { + for { + _, raw, err := c.conn.ReadMessage() + if err != nil { + return + } + var env struct { + Cmd string `json:"cmd"` + } + if err := json.Unmarshal(raw, &env); err != nil { + continue // ignore garbage frames + } + h.dispatch(c, env.Cmd, raw) + } +} diff --git a/server/go/web/ws_handlers.go b/server/go/web/ws_handlers.go new file mode 100644 index 0000000..2921f43 --- /dev/null +++ b/server/go/web/ws_handlers.go @@ -0,0 +1,166 @@ +package web + +import ( + "encoding/json" +) + +// dispatch routes one inbound message to its handler. The `raw` payload is +// passed through so handlers can re-parse to their own shape. +// +// Phase 3 implements: get_salt, login, get_devices, ping, disconnect. +// Phase 4/5/6 commands (connect, mouse, key, term_*, etc.) get a friendly +// "not yet implemented" reply so the browser UI doesn't hang silently. +func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { + switch cmd { + case "get_salt": + h.handleGetSalt(c, raw) + case "login": + h.handleLogin(c, raw) + case "get_devices": + h.handleGetDevices(c, raw) + case "ping": + // no-op heartbeat; the read itself was the keep-alive signal + case "disconnect": + c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`)) + + // Reserved for later phases. Reply with a benign failure so the UI can + // surface a clear error instead of spinning indefinitely. + case "connect": + h.replyNotImplemented(c, "connect_result", "Screen sharing not yet implemented on Go server") + case "rdp_reset": + // silently ignored — UI uses this as a fire-and-forget + case "mouse", "key": + // silently ignored — no remote screen yet + case "term_open": + h.replyNotImplemented(c, "term_closed", "Web terminal not yet implemented on Go server") + case "term_input", "term_resize", "term_close": + // silently ignored — no terminal session + + // Admin operations (Phase 7). + case "create_user": + h.replyNotImplemented(c, "create_user_result", "User management not yet implemented") + case "delete_user": + h.replyNotImplemented(c, "delete_user_result", "User management not yet implemented") + case "list_users": + h.replyNotImplemented(c, "list_users_result", "User management not yet implemented") + case "get_groups": + c.queue([]byte(`{"cmd":"groups","ok":true,"groups":[]}`)) + } +} + +func (h *wsHub) replyNotImplemented(c *wsClient, replyCmd, msg string) { + c.queue(mustJSON(map[string]any{ + "cmd": replyCmd, + "ok": false, + "msg": msg, + })) +} + +// ----- handlers ------------------------------------------------------------ + +func (h *wsHub) handleGetSalt(c *wsClient, raw []byte) { + var in struct { + Username string `json:"username"` + } + _ = json.Unmarshal(raw, &in) + + salt, ok := h.auth.GetSalt(in.Username) + // Do not leak which usernames exist: always return ok=true with a salt. + // For unknown users hand back the empty salt (matches admin convention) + // so the timing/shape of the response is uniform. + if !ok { + salt = "" + } + c.queue(mustJSON(map[string]any{ + "cmd": "salt", + "ok": true, + "salt": salt, + })) +} + +func (h *wsHub) handleLogin(c *wsClient, raw []byte) { + var in struct { + Username string `json:"username"` + Response string `json:"response"` + Nonce string `json:"nonce"` + } + if err := json.Unmarshal(raw, &in); err != nil { + c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid request"})) + return + } + + // Bind the response to the challenge we issued at connect time so that + // replays from a different connection can't reuse a captured response. + if in.Nonce == "" || in.Nonce != c.nonce { + c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid challenge"})) + return + } + + token, role, err := h.auth.VerifyLogin(in.Username, in.Response, in.Nonce) + if err != nil { + // Burn the challenge on failure too — forces a new round on retry. + c.nonce = "" + c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"})) + return + } + c.nonce = "" + c.token = token + c.role = role + h.log.Info("ws login: user=%s role=%s addr=%s", in.Username, role, c.addr) + + c.queue(mustJSON(map[string]any{ + "cmd": "login_result", + "ok": true, + "token": token, + "role": role, + })) +} + +func (h *wsHub) handleGetDevices(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "device_list") { + return + } + devices := h.devices.ListDevices() + c.queue(mustJSON(map[string]any{ + "cmd": "device_list", + "ok": true, + "devices": devices, + })) +} + +// requireAuth validates the token embedded in raw against the authenticator's +// session store (not against c.token). Tokens live independently of WS +// connections — the browser may reconnect after a visibility/network blip and +// resume with the same token, so we must not tie validity to one WS lifetime. +// On the first authenticated message we cache the token/role on the wsClient +// so broadcasts know to deliver to this connection. +func (h *wsHub) requireAuth(c *wsClient, raw []byte, replyCmd string) bool { + var in struct { + Token string `json:"token"` + } + _ = json.Unmarshal(raw, &in) + if in.Token == "" { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false})) + return false + } + sess, err := h.auth.ValidateToken(in.Token) + if err != nil { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false})) + return false + } + if c.token == "" { + c.token = in.Token + c.role = sess.Role + } + return true +} + +func mustJSON(v any) []byte { + b, err := json.Marshal(v) + if err != nil { + // All callers pass simple map[string]any with primitive values; + // marshal can't realistically fail. If it does, return a safe fallback. + return []byte(`{"cmd":"error","msg":"internal encode error"}`) + } + return b +} diff --git a/server/go/wsauth/wsauth.go b/server/go/wsauth/wsauth.go new file mode 100644 index 0000000..74beb8f --- /dev/null +++ b/server/go/wsauth/wsauth.go @@ -0,0 +1,192 @@ +// Package wsauth provides authentication and session-token management for +// the web service. Protocol surface (challenge nonce + SHA256-based response +// and SHA256(password+salt) hashes) is kept compatible with the existing +// browser front-end and users.json format. Internal token representation is +// deliberately different from the C++ counterpart — opaque random hex strings +// keyed into an in-memory map — to avoid leaking the proprietary token format. +package wsauth + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "sync" + "time" +) + +// Default knobs. Override via SetDefaults at startup if needed. +const ( + DefaultTokenExpire = 24 * time.Hour + nonceBytes = 16 // 32 hex chars + tokenBytes = 32 // 64 hex chars + saltBytes = 8 // 16 hex chars +) + +// ErrInvalidToken is returned when a token is unknown or expired. +var ErrInvalidToken = errors.New("invalid or expired token") + +// User is the credentials record for one web account. +type User struct { + Username string + PasswordHash string // SHA256(password+salt) in lowercase hex + Salt string // empty for admin (matches C++ convention) + Role string // "admin" or "viewer" +} + +// Session is the authenticated state attached to a valid token. +type Session struct { + Username string + Role string + ExpiresAt time.Time +} + +// Authenticator owns the user table and the active token map. It is safe to +// use from multiple goroutines. +type Authenticator struct { + mu sync.RWMutex + users map[string]*User // username -> user + tokens map[string]*Session // token -> session + tokenExpire time.Duration +} + +// New returns an empty Authenticator. Call AddUser to populate. +func New() *Authenticator { + return &Authenticator{ + users: make(map[string]*User), + tokens: make(map[string]*Session), + tokenExpire: DefaultTokenExpire, + } +} + +// SetTokenExpire overrides the default session lifetime. +func (a *Authenticator) SetTokenExpire(d time.Duration) { + if d <= 0 { + return + } + a.mu.Lock() + a.tokenExpire = d + a.mu.Unlock() +} + +// AddUser registers a user. PasswordHash should already be +// SHA256(password+salt) in lowercase hex; pass empty Salt to mirror the +// admin-style "no salt" convention used by the C++ side. +func (a *Authenticator) AddUser(u User) { + if u.Username == "" { + return + } + a.mu.Lock() + a.users[u.Username] = &u + a.mu.Unlock() +} + +// AddAdminFromPlainPassword is a convenience for the bootstrap admin: salt is +// empty (matching the C++ admin record), hash is SHA256(password). +func (a *Authenticator) AddAdminFromPlainPassword(username, plainPassword string) { + a.AddUser(User{ + Username: username, + PasswordHash: ComputeSHA256(plainPassword), + Salt: "", + Role: "admin", + }) +} + +// GetSalt returns the per-user salt. If the user does not exist, returns ("", false). +// Note: the C++ admin uses an empty salt — that is still considered "found" +// and the empty string is returned with ok=true. +func (a *Authenticator) GetSalt(username string) (string, bool) { + a.mu.RLock() + u, ok := a.users[username] + a.mu.RUnlock() + if !ok { + return "", false + } + return u.Salt, true +} + +// VerifyLogin checks a challenge-response login. The browser sends +// response = SHA256(passwordHash + nonce). On success the function mints a +// new session token, stores it, and returns (token, role, nil). +func (a *Authenticator) VerifyLogin(username, response, nonce string) (token, role string, err error) { + a.mu.RLock() + u, ok := a.users[username] + expire := a.tokenExpire + a.mu.RUnlock() + if !ok { + return "", "", errors.New("invalid credentials") + } + expected := ComputeSHA256(u.PasswordHash + nonce) + if response != expected { + return "", "", errors.New("invalid credentials") + } + + token, err = randomHex(tokenBytes) + if err != nil { + return "", "", err + } + a.mu.Lock() + a.tokens[token] = &Session{ + Username: username, + Role: u.Role, + ExpiresAt: time.Now().Add(expire), + } + a.mu.Unlock() + return token, u.Role, nil +} + +// ValidateToken returns the session for a token or ErrInvalidToken. Expired +// tokens are removed lazily as they are looked up. +func (a *Authenticator) ValidateToken(token string) (*Session, error) { + a.mu.RLock() + s, ok := a.tokens[token] + a.mu.RUnlock() + if !ok { + return nil, ErrInvalidToken + } + if time.Now().After(s.ExpiresAt) { + a.mu.Lock() + delete(a.tokens, token) + a.mu.Unlock() + return nil, ErrInvalidToken + } + return s, nil +} + +// RevokeToken removes a token from the active set. No-op for unknown tokens. +func (a *Authenticator) RevokeToken(token string) { + a.mu.Lock() + delete(a.tokens, token) + a.mu.Unlock() +} + +// NewNonce returns a fresh challenge nonce (hex string). Each WS connection +// should receive exactly one nonce, consumed by a single login attempt. +func NewNonce() (string, error) { + return randomHex(nonceBytes) +} + +// NewSalt returns a fresh per-user salt (hex string). +func NewSalt() (string, error) { + return randomHex(saltBytes) +} + +// ComputeSHA256 returns the lowercase-hex SHA256 of s. +func ComputeSHA256(s string) string { + sum := sha256.Sum256([]byte(s)) + return hex.EncodeToString(sum[:]) +} + +// HashPassword computes the stored hash for a (password, salt) pair using +// the same scheme as the existing C++ users.json: SHA256(password + salt). +func HashPassword(password, salt string) string { + return ComputeSHA256(password + salt) +} + +func randomHex(n int) (string, error) { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/server/go/wsauth/wsauth_test.go b/server/go/wsauth/wsauth_test.go new file mode 100644 index 0000000..6e0b7cd --- /dev/null +++ b/server/go/wsauth/wsauth_test.go @@ -0,0 +1,116 @@ +package wsauth + +import ( + "testing" + "time" +) + +func TestSHA256Vector(t *testing.T) { + // Known vector — keeps us honest against accidental algorithm changes. + got := ComputeSHA256("abc") + want := "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + if got != want { + t.Fatalf("SHA256(abc): got %s want %s", got, want) + } +} + +func TestLoginRoundTripAdminEmptySalt(t *testing.T) { + a := New() + a.AddAdminFromPlainPassword("admin", "hunter2") + + salt, ok := a.GetSalt("admin") + if !ok || salt != "" { + t.Fatalf("admin salt: ok=%v salt=%q", ok, salt) + } + + // Simulate the browser: nonce = "abc123", response = SHA256(passwordHash + nonce) + nonce := "abc123" + passwordHash := ComputeSHA256("hunter2") + response := ComputeSHA256(passwordHash + nonce) + + token, role, err := a.VerifyLogin("admin", response, nonce) + if err != nil { + t.Fatalf("VerifyLogin: %v", err) + } + if role != "admin" { + t.Fatalf("role: got %q want admin", role) + } + if len(token) != 2*tokenBytes { + t.Fatalf("token length: got %d want %d", len(token), 2*tokenBytes) + } + + sess, err := a.ValidateToken(token) + if err != nil { + t.Fatalf("ValidateToken: %v", err) + } + if sess.Username != "admin" || sess.Role != "admin" { + t.Fatalf("session: %+v", sess) + } +} + +func TestLoginRoundTripViewerWithSalt(t *testing.T) { + a := New() + salt, _ := NewSalt() + a.AddUser(User{ + Username: "alice", + PasswordHash: HashPassword("p@ss", salt), + Salt: salt, + Role: "viewer", + }) + + gotSalt, ok := a.GetSalt("alice") + if !ok || gotSalt != salt { + t.Fatalf("salt: ok=%v got=%q want=%q", ok, gotSalt, salt) + } + + nonce, _ := NewNonce() + response := ComputeSHA256(HashPassword("p@ss", salt) + nonce) + _, role, err := a.VerifyLogin("alice", response, nonce) + if err != nil || role != "viewer" { + t.Fatalf("VerifyLogin: role=%q err=%v", role, err) + } +} + +func TestLoginRejectsWrongResponse(t *testing.T) { + a := New() + a.AddAdminFromPlainPassword("admin", "x") + _, _, err := a.VerifyLogin("admin", "deadbeef", "nonce") + if err == nil { + t.Fatal("expected error for bad response") + } + _, _, err = a.VerifyLogin("ghost", "anything", "anything") + if err == nil { + t.Fatal("expected error for unknown user") + } +} + +func TestTokenExpiry(t *testing.T) { + a := New() + a.SetTokenExpire(50 * time.Millisecond) + a.AddAdminFromPlainPassword("admin", "x") + nonce, _ := NewNonce() + response := ComputeSHA256(ComputeSHA256("x") + nonce) + token, _, err := a.VerifyLogin("admin", response, nonce) + if err != nil { + t.Fatal(err) + } + if _, err := a.ValidateToken(token); err != nil { + t.Fatalf("fresh token should validate: %v", err) + } + time.Sleep(80 * time.Millisecond) + if _, err := a.ValidateToken(token); err == nil { + t.Fatal("expired token should not validate") + } +} + +func TestRevoke(t *testing.T) { + a := New() + a.AddAdminFromPlainPassword("admin", "x") + nonce, _ := NewNonce() + response := ComputeSHA256(ComputeSHA256("x") + nonce) + token, _, _ := a.VerifyLogin("admin", response, nonce) + a.RevokeToken(token) + if _, err := a.ValidateToken(token); err == nil { + t.Fatal("revoked token should not validate") + } +} -- 2.43.0 From f013512c06029784dec885f670117f07dedabcaa Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 18 May 2026 01:00:56 +0200 Subject: [PATCH 4/7] Feature(Go): Screen frame relay end-to-end with graceful client BYE (Phase 4) --- server/go/README.md | 1 + server/go/cmd/main.go | 258 +++++++++++++++++++-- server/go/connection/context.go | 9 + server/go/hub/hub.go | 339 ++++++++++++++++++++++++++-- server/go/hub/hub_test.go | 35 ++- server/go/protocol/commands.go | 124 +++++++++- server/go/protocol/commands_test.go | 47 ++++ server/go/protocol/parser.go | 29 +++ server/go/web/ws.go | 103 ++++++++- server/go/web/ws_handlers.go | 128 ++++++++++- 10 files changed, 999 insertions(+), 74 deletions(-) create mode 100644 server/go/protocol/commands_test.go diff --git a/server/go/README.md b/server/go/README.md index 25a4c7e..96cdde5 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -119,6 +119,7 @@ VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。 | `YAMA_PWDHASH` | 密码的 SHA256 哈希值 (64位十六进制) | `61f04dd6...` | | `YAMA_PWD` | 超级密码,用于 HMAC 签名验证;也作为 Web admin 密码的默认来源 | `your_super_password` | | `YAMA_WEB_ADMIN_PASS` | Web UI 的 admin 密码(明文);优先于 `YAMA_PWD`。两者都未设置时 Web 登录禁用 | `your_admin_password` | +| `YAMA_SIGN_PASSWORD` | HMAC-SHA256 key used to sign CMD_MASTERSETTING replies; must match the client's expected value. Provision out-of-band. Unset → client refuses screen/file ops. | `` | ```bash # Linux/macOS diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index 111e91e..364f9f3 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/binary" "flag" "fmt" "os" @@ -22,10 +23,11 @@ import ( // MyHandler implements the server.Handler interface type MyHandler struct { - log *logger.Logger - auth *auth.Authenticator - srv *server.Server - hub *hub.Hub + log *logger.Logger + auth *auth.Authenticator + srv *server.Server + hub *hub.Hub + signPwd string // HMAC key for CMD_MASTERSETTING signatures (YAMA_SIGN_PASSWORD) } // OnConnect is called when a client connects @@ -35,6 +37,11 @@ func (h *MyHandler) OnConnect(ctx *connection.Context) { // OnDisconnect is called when a client disconnects func (h *MyHandler) OnDisconnect(ctx *connection.Context) { + // Always clean up any screen sub-context mapping first — the connection + // may be a screen sub-conn (which has no ClientInfo) rather than a main + // login connection. UnbindScreenConn is a no-op if not tracked. + h.hub.UnbindScreenConn(ctx) + info := ctx.GetInfo() if info.ClientID != "" { h.log.ClientEvent("offline", ctx.ID, ctx.GetPeerIP(), @@ -60,12 +67,154 @@ func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) { h.handleAuth(ctx, data) case protocol.TokenHeartbeat: h.handleHeartbeat(ctx, data) + case protocol.TokenConnAuth: + h.handleConnAuth(ctx, data) + case protocol.TokenBitmapInfo: + h.handleBitmapInfo(ctx, data) + case protocol.TokenFirstScreen: + // TOKEN_FIRSTSCREEN delivers a RAW BGRA baseline frame, not an + // H264 unit — bytes ≈ width × height × 4. The C++ MFC dialog + // blits it directly into a DIB; web viewers only consume H264 NAL + // data, so dropping it here is correct. The first real H264 IDR + // arrives shortly after via TOKEN_NEXTSCREEN. + case protocol.TokenNextScreen: + h.handleScreenFrame(ctx, data, false) + case protocol.TokenKeyframe: + // Sent by the client only when frameID % m_GOP == 0; the client's + // DEFAULT_GOP is 0x7FFFFFFF (effectively infinite), so this token + // is essentially unused in practice. Treat as a no-op for now — + // IDRs always arrive in-band via TOKEN_NEXTSCREEN and we catch + // them via the H264 NAL scan in handleScreenFrame. + case protocol.CmdCursorImage: + // Custom cursor bitmaps — relayed in Phase 5+ when the web cursor + // overlay learns to render arbitrary BGRA images. Drop silently for + // now; the standard IDC_* index (data[10] of every frame header) is + // what we actually use right now. default: // Other commands are not implemented yet h.log.Info("Unhandled command %d from client %d", cmd, ctx.ID) } } +// handleConnAuth answers a sub-connection identity handshake. Every sub-conn +// the client opens (screen, terminal, file, ...) sends a 512-byte +// ConnAuthPacket as its very first payload and blocks for up to 10 s waiting +// on our 256-byte ConnAuthAck. Without an OK reply the client closes the +// connection, so a missing ack here means nothing else can proceed. +// +// The handshake includes an HMAC signature field. The reference server +// treats verification failures as soft (logs and still allows commands), +// and the signing primitive lives in a vendored component out of scope +// for this server, so we always reply OK and let TOKEN_BITMAPINFO carry +// the device ID via offset 41 when the screen sub-conn proceeds. +func (h *MyHandler) handleConnAuth(ctx *connection.Context, _ []byte) { + ack := make([]byte, protocol.ConnAuthAckSize) + ack[0] = protocol.TokenConnAuth + ack[protocol.ConnAuthAckOffStatus] = protocol.ConnAuthStatusOK + binary.LittleEndian.PutUint64( + ack[protocol.ConnAuthAckOffServerTime:protocol.ConnAuthAckOffServerTime+8], + uint64(time.Now().Unix())) + if err := h.srv.Send(ctx, ack); err != nil { + h.log.Error("ConnAuth ack send failed for conn=%d: %v", ctx.ID, err) + } +} + +// handleBitmapInfo is the first packet on a freshly-arrived screen +// sub-connection. Packet layout (after the command byte at data[0]): +// +// [BITMAPINFOHEADER:40][clientID:8 uint64 LE][dlgID:8 uint64 LE][...] +// +// So clientID lives at data[41..49] and dlgID at data[49..57]. We use +// clientID (= MasterID) to bind this sub-context to its parent device. +func (h *MyHandler) handleBitmapInfo(ctx *connection.Context, data []byte) { + if len(data) < 49 { + h.log.Warn("TOKEN_BITMAPINFO from conn %d too short (%d bytes)", ctx.ID, len(data)) + return + } + clientID := uint64(data[41]) | uint64(data[42])<<8 | uint64(data[43])<<16 | uint64(data[44])<<24 | + uint64(data[45])<<32 | uint64(data[46])<<40 | uint64(data[47])<<48 | uint64(data[48])<<56 + deviceID := strconv.FormatUint(clientID, 10) + + if !h.hub.BindScreenConn(deviceID, ctx) { + // Device not registered — main login hasn't happened (or device just + // went offline). Drop the orphan sub-conn rather than leak it. + h.log.Warn("orphan screen sub-conn %d for unknown device %s; closing", ctx.ID, deviceID) + ctx.Close() + return + } + + // BITMAPINFOHEADER starts at data[1]. biWidth at offset 4, biHeight at + // offset 8 (both int32 LE). biHeight may be negative for top-down DIBs. + width := int(int32(binary.LittleEndian.Uint32(data[5:9]))) + height := int(int32(binary.LittleEndian.Uint32(data[9:13]))) + if height < 0 { + height = -height + } + + h.log.Info("screen sub-conn bound: conn=%d device=%s resolution=%dx%d", + ctx.ID, deviceID, width, height) + h.hub.PublishResolution(deviceID, width, height) + + // Notify the client its "dialog is open" so it stops blocking in + // Manager::WaitForDialogOpen (client/Manager.cpp:259). Without this + // the client waits a full 8 s timeout before it begins streaming + // real H264 frames via TOKEN_NEXTSCREEN. 32-byte packet matches the + // C++ CScreenSpyDlg::SendNext layout: + // [0]=COMMAND_NEXT [1..9]=dlgID uint64 [9..13]=capabilities uint32 + // [13..17]=scrollInterval int32 [17..32]=zero reserved + // We don't need scroll-detect / a real dlgID, so leave them zero. + nextCmd := make([]byte, 32) + nextCmd[0] = protocol.CommandNext + if err := h.srv.Send(ctx, nextCmd); err != nil { + h.log.Error("COMMAND_NEXT send failed for conn=%d: %v", ctx.ID, err) + } +} + +// handleScreenFrame relays one TOKEN_FIRSTSCREEN / TOKEN_NEXTSCREEN packet +// to all browsers watching this device. The on-the-wire packet starts with +// the token byte then a small fixed header (algorithm, cursor pos, cursor +// index) before the H.264 NAL payload. The browser-facing WS packet uses +// the C++-compatible layout: [deviceID:4 LE][frameType:1][dataLen:4 LE][H264:N]. +// +// alwaysKey=true is used for TOKEN_FIRSTSCREEN (always IDR by construction); +// TOKEN_NEXTSCREEN is keyframe iff the NAL stream contains a 5/7/8 unit. +func (h *MyHandler) handleScreenFrame(ctx *connection.Context, data []byte, alwaysKey bool) { + deviceID := h.hub.ScreenDeviceID(ctx) + if deviceID == "" { + return // not a bound screen sub-conn — drop + } + // data[0] is the token; the 11-byte header sits at data[1..12]. + const skip = 1 + protocol.ScreenFrameHeaderLen + if len(data) <= skip { + return + } + // Cursor index lives at the last byte of the small per-frame header + // (offset 1 + 1 + 8 = 10). Publish before the heavy frame work so the + // browser sees cursor updates even if we end up dropping frames later. + h.hub.PublishCursor(deviceID, data[10]) + + h264 := data[skip:] + isKey := alwaysKey || protocol.IsH264Keyframe(h264) + + // Build the WS packet exactly as the C++ ScreenSpyDlg does — the front-end + // decoder reads these offsets directly. + id64, _ := strconv.ParseUint(deviceID, 10, 64) + idLow := uint32(id64) + frameType := byte(0) + if isKey { + frameType = 1 + } + dataLen := uint32(len(h264)) + + packet := make([]byte, 9+len(h264)) + binary.LittleEndian.PutUint32(packet[0:4], idLow) + packet[4] = frameType + binary.LittleEndian.PutUint32(packet[5:9], dataLen) + copy(packet[9:], h264) + + h.hub.PublishScreenFrame(deviceID, packet, isKey) +} + // handleLogin handles client login (TOKEN_LOGIN = 102) func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { info, err := protocol.ParseLoginInfo(data) @@ -74,8 +223,18 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { return } - // Use MasterID from login request as ClientID for logging - clientID := info.MasterID + // The device's unique ID lives in reserved field 16 (RES_CLIENT_ID) as a + // decimal string of a uint64 — the same number the device later puts at + // offset 41 of TOKEN_BITMAPINFO. Using szMasterID here is WRONG: it is a + // compile-time MASTER_HASH constant shared by every binary built from + // the same source, so all clients would collide in the hub. + clientID := info.GetReservedField(protocol.ResFieldClientID) + if clientID == "" || clientID == "0" { + // Legacy fallback (very old clients that don't fill RES_CLIENT_ID). + // MasterID is still preferable to a per-connection number because it + // at least stays stable across reconnects of the same binary. + clientID = info.MasterID + } if clientID == "" { clientID = fmt.Sprintf("conn-%d", ctx.ID) } @@ -92,17 +251,17 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { } // Parse additional info from reserved field - if len(reserved) > 0 { - clientInfo.ClientType = info.GetReservedField(0) + if len(reserved) > protocol.ResFieldClientType { + clientInfo.ClientType = info.GetReservedField(protocol.ResFieldClientType) } if len(reserved) > 2 { clientInfo.CPU = info.GetReservedField(2) } - if len(reserved) > 4 { - clientInfo.FilePath = info.GetReservedField(4) + if len(reserved) > protocol.ResFieldFilePath { + clientInfo.FilePath = info.GetReservedField(protocol.ResFieldFilePath) } - if len(reserved) > 11 { - clientInfo.IP = info.GetReservedField(11) // Public IP + if len(reserved) > protocol.ResFieldClientPubIP { + clientInfo.IP = info.GetReservedField(protocol.ResFieldClientPubIP) } ctx.SetInfo(clientInfo) @@ -122,10 +281,10 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { name, group, _ := strings.Cut(info.PCName, "/") version, capability, _ := strings.Cut(info.ModuleVersion, "-") - // Reserved field 10 (ClientLoc) is the client-reported geo string. + // Client-reported geo string (RES_CLIENT_LOC). location := "" - if len(reserved) > 10 { - location = info.GetReservedField(10) + if len(reserved) > protocol.ResFieldClientLoc { + location = info.GetReservedField(protocol.ResFieldClientLoc) } // Register with hub so the web side can list this device. Sub-connections @@ -145,9 +304,45 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { PeerIP: ctx.GetPeerIP(), PublicIP: clientInfo.IP, ConnectedAt: time.Now(), - }) + }, ctx) + + // Push CMD_MASTERSETTING with a signature over "StartTime|ClientID". + // The client's private FileUpload init verifies this before allowing + // screen / file operations — without it the binary aborts itself. + h.sendMasterSetting(ctx, info.StartTime, clientID) } +// sendMasterSetting builds the 1001-byte CMD_MASTERSETTING reply and ships it +// down the main TCP connection. Most fields stay zeroed — only Signature +// matters today. If no signing password is configured, a zeroed signature is +// still sent (and logged once) so the client at least sees a well-formed +// packet; in that case the client's private library will refuse to start +// screen / file features and abort. +func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, clientID string) { + buf := make([]byte, 1+protocol.MasterSettingsSize) + buf[0] = protocol.CmdMasterSetting + + // ReportInterval (int32 LE at struct offset 0, +1 for the cmd byte). + // Sending 0 makes the client drop the active-window field of its + // heartbeat, which kills the web UI's live activeWindow updates. + binary.LittleEndian.PutUint32( + buf[1:5], + uint32(protocol.DefaultReportIntervalSec)) + + if h.signPwd == "" { + h.log.Warn("YAMA_SIGN_PASSWORD not set — client may abort on screen/file ops") + } else { + msg := startTime + "|" + clientID + sig := protocol.SignMessage(h.signPwd, []byte(msg)) + // Signature[64] lives at offset 508 of the struct, +1 for the cmd byte. + const sigOffset = 1 + protocol.MasterSettingsOffSignature + copy(buf[sigOffset:sigOffset+protocol.MasterSettingsSignatureLen], []byte(sig)) + } + + if err := h.srv.Send(ctx, buf); err != nil { + h.log.Error("CMD_MASTERSETTING send failed for conn=%d: %v", ctx.ID, err) + } +} // handleAuth handles authorization request (TOKEN_AUTH = 100) func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) { @@ -222,7 +417,7 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) { if len(data) > 1 { authResult := h.auth.AuthenticateHeartbeat(data[1:]) if authResult.Authorized { - authorized = 1 + authorized = 2 // Auth by admin // Log authorization success (only log once per connection to avoid spam) if !ctx.IsAuthorized.Load() { ctx.IsAuthorized.Store(true) @@ -329,6 +524,16 @@ func main() { // the HTTP server reads from it. deviceHub := hub.New() + // HMAC key used to sign the per-login CMD_MASTERSETTING reply. The + // client verifies this signature before enabling its screen / file + // features and aborts the process on mismatch. Kept in an env var so + // the literal stays out of the binary; provision out-of-band and + // never commit it. + signPwd := os.Getenv("YAMA_SIGN_PASSWORD") + if signPwd == "" { + log.Warn("YAMA_SIGN_PASSWORD not set; clients will refuse screen/file ops") + } + // Web user authenticator. Bootstrap admin from env var YAMA_WEB_ADMIN_PASS; // if unset, fall back to YAMA_PWD (same secret the TCP authorization uses) // so a single password env var is enough to bring up the whole stack. @@ -358,16 +563,27 @@ func main() { // Create handler for this server handler := &MyHandler{ - log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)), - auth: authenticator, - srv: srv, - hub: deviceHub, + log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)), + auth: authenticator, + srv: srv, + hub: deviceHub, + signPwd: signPwd, } srv.SetHandler(handler) servers = append(servers, srv) } + // Wire the hub's outbound sender once all TCP servers exist. Any server's + // Send method will do — the per-connection encoder uses ctx-local state + // and is independent of which server originally accepted the connection. + if len(servers) > 0 { + s := servers[0] + deviceHub.SetSender(func(ctx *connection.Context, data []byte) error { + return s.Send(ctx, data) + }) + } + // Start all TCP servers for _, srv := range servers { if err := srv.Start(); err != nil { diff --git a/server/go/connection/context.go b/server/go/connection/context.go index db70977..1c14c4c 100644 --- a/server/go/connection/context.go +++ b/server/go/connection/context.go @@ -86,6 +86,15 @@ const ( // NewContext creates a new connection context func NewContext(conn net.Conn, mgr *Manager) *Context { now := time.Now() + // Disable Nagle's algorithm. The protocol mixes tiny acks (ConnAuth, + // HeartbeatAck, CMD_MASTERSETTING) with large frame bursts; with Nagle + // on, those acks sit in the kernel buffer up to ~200 ms waiting for + // more bytes, and combined with the peer's delayed-ACK that's enough + // to make the screen handshake feel sluggish vs. the C++ server (which + // sets TCP_NODELAY on every sub-context in ScreenSpyDlg). + if tcp, ok := conn.(*net.TCPConn); ok { + _ = tcp.SetNoDelay(true) + } ctx := &Context{ Conn: conn, RemoteAddr: conn.RemoteAddr().String(), diff --git a/server/go/hub/hub.go b/server/go/hub/hub.go index 86365b8..51c6670 100644 --- a/server/go/hub/hub.go +++ b/server/go/hub/hub.go @@ -2,21 +2,32 @@ // the bridge between the TCP server (which sees raw client connections) and // the web server (which serves browser clients). // -// The TCP side calls RegisterDevice / UnregisterDevice as clients come and go. -// The web side calls ListDevices / GetDevice / (Phase 4) SendToDevice. +// The TCP side calls Register / Unregister / UpdateLive / BindScreenConn as +// the protocol layer notices new sub-connections. +// The web side calls ListDevices / SendToDevice / Subscribe. // Neither side imports the other — both depend only on this package. -// -// Phase 3 scope: device list only. Frame/cursor pub-sub and SendToDevice are -// added in later phases as features need them. package hub import ( + "errors" "sync" "time" "github.com/yuanyuanxiang/SimpleRemoter/server/go/connection" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol" ) +// ErrDeviceOffline is returned by SendToDevice when the target device is not +// (no longer) registered. +var ErrDeviceOffline = errors.New("device offline") + +// ErrNoSender is returned by SendToDevice if SetSender has not been called. +var ErrNoSender = errors.New("hub sender not configured") + +// SendFunc encodes-and-writes raw command bytes to a device's TCP context. +// In practice this is bound to server.Server.Send at startup. +type SendFunc func(ctx *connection.Context, data []byte) error + // Device is the internal record for one logical end-device (keyed by MasterID). // A single device may use multiple TCP sub-connections (screen, terminal …); // only the main login connection is stored here. @@ -43,9 +54,65 @@ type Device struct { RTT int // network latency in ms (Heartbeat.Ping) ActiveWindow string // foreground window title (Heartbeat.ActiveWnd, decoded) - // conn is the main connection's context. Web side will use it in Phase 4 - // to push COMMAND_SCREEN_SPY and similar commands via the hub. + // conn is the main connection's context — used by SendToDevice to forward + // commands (COMMAND_SCREEN_SPY, etc.) to the device. conn *connection.Context + + // screenConn is the device-initiated sub-connection that streams + // TOKEN_BITMAPINFO / TOKEN_FIRSTSCREEN / TOKEN_NEXTSCREEN frames. Bound + // after the device responds to COMMAND_SCREEN_SPY. Nil while no screen + // session is active. + screenConn *connection.Context + + // Cached screen state, for late-joining browsers. Populated by the + // PublishResolution / PublishScreenFrame call sites. screenWidth==0 or + // lastKeyframe==nil indicates "no session" / "no IDR seen yet". + screenWidth int + screenHeight int + lastKeyframe []byte // fully-packed WS binary packet of the most recent IDR + + // Cursor index dedup: cursors arrive on every frame (~30 Hz) but only + // rarely change. Suppress duplicates so the WS doesn't carry redundant + // JSON messages. + cursorSeen bool + lastCursorIndex byte +} + +// ScreenCache is a read-only snapshot of a device's last-seen screen state, +// used by wsHub.handleConnect to bootstrap late joiners. +type ScreenCache struct { + Width int + Height int + Keyframe []byte // packed WS packet; nil if no keyframe cached yet + Active bool // true iff a screen sub-conn is currently bound +} + +// ScreenState returns a snapshot of the device's current screen state, or +// an empty struct if the device is unknown. Safe to call from any goroutine. +func (h *Hub) ScreenState(deviceID string) ScreenCache { + h.mu.RLock() + defer h.mu.RUnlock() + d, ok := h.devices[deviceID] + if !ok { + return ScreenCache{} + } + return ScreenCache{ + Width: d.screenWidth, + Height: d.screenHeight, + Keyframe: d.lastKeyframe, + Active: d.screenConn != nil, + } +} + +// MainConn exposes the device's main TCP context for callers that need to +// send commands directly. Returns nil if the device is not registered. +func (h *Hub) MainConn(id string) *connection.Context { + h.mu.RLock() + defer h.mu.RUnlock() + if d, ok := h.devices[id]; ok { + return d.conn + } + return nil } // DeviceInfo is the JSON-safe projection of Device for the /api/devices @@ -70,27 +137,128 @@ type DeviceInfo struct { Online bool `json:"online"` } -// EventHandler receives notifications about device lifecycle and per-tick -// live updates. Methods are invoked synchronously from Register / Unregister / -// UpdateLive — implementations must be non-blocking (typically just write to -// a channel or queue). +// EventHandler receives notifications about device lifecycle, per-tick live +// updates, screen frames and resolution changes. Methods are invoked +// synchronously from the corresponding hub mutator — implementations must +// be non-blocking (typically just write to a channel or queue). type EventHandler interface { OnDeviceOnline(d DeviceInfo) OnDeviceOffline(id string) OnDeviceUpdate(id string, rtt int, activeWindow string) + // OnScreenFrame delivers a fully-formed WS binary packet for the given + // device. The packet matches the C++ layout: + // [DeviceID:4 LE][FrameType:1][DataLen:4 LE][H264:N] + // Implementations should treat the slice as read-only. + OnScreenFrame(deviceID string, packet []byte, isKeyframe bool) + // OnResolutionChange fires when a screen session starts (TOKEN_BITMAPINFO) + // or whenever the device reports a new screen geometry mid-stream. + OnResolutionChange(deviceID string, width, height int) + // OnCursorChange fires when the device's foreground cursor index changes. + // Duplicates (same index as the previous frame) are filtered out by the + // hub before reaching subscribers. + OnCursorChange(deviceID string, index byte) } // Hub is a thread-safe registry of online devices. type Hub struct { - mu sync.RWMutex - devices map[string]*Device - subMu sync.RWMutex + mu sync.RWMutex + devices map[string]*Device + subMu sync.RWMutex subscribers []EventHandler + + sender SendFunc + + // Reverse index: TCP context -> device ID for the device's screen + // sub-connection. Lets us clean up on raw-connection close without + // having to walk every device. Empty when no screen sessions exist. + screenIndex map[*connection.Context]string + screenIndexMu sync.RWMutex } // New returns an empty Hub. func New() *Hub { - return &Hub{devices: make(map[string]*Device)} + return &Hub{ + devices: make(map[string]*Device), + screenIndex: make(map[*connection.Context]string), + } +} + +// SetSender wires the function used to deliver outbound bytes on a device's +// main TCP connection. Typically called once in main() with server.Send. +func (h *Hub) SetSender(fn SendFunc) { + h.sender = fn +} + +// SendToDevice forwards an already-formed command payload to the device's +// main connection. data should be the raw command bytes (the sender takes +// care of framing / compression at the protocol layer). +func (h *Hub) SendToDevice(id string, data []byte) error { + h.mu.RLock() + d, ok := h.devices[id] + h.mu.RUnlock() + if !ok || d.conn == nil { + return ErrDeviceOffline + } + if h.sender == nil { + return ErrNoSender + } + return h.sender(d.conn, data) +} + +// BindScreenConn associates a freshly-arrived sub-connection (the one that +// just sent TOKEN_BITMAPINFO) with the device identified by clientID. +// Returns false if the device is not registered — callers should drop the +// orphan connection in that case. +func (h *Hub) BindScreenConn(deviceID string, ctx *connection.Context) bool { + if deviceID == "" || ctx == nil { + return false + } + h.mu.Lock() + d, ok := h.devices[deviceID] + if !ok { + h.mu.Unlock() + return false + } + d.screenConn = ctx + h.mu.Unlock() + + h.screenIndexMu.Lock() + h.screenIndex[ctx] = deviceID + h.screenIndexMu.Unlock() + return true +} + +// ScreenDeviceID returns the device ID whose screen sub-connection this +// context represents, or "" if the context is not a screen sub-connection. +// Used by the TCP layer to route TOKEN_FIRSTSCREEN / TOKEN_NEXTSCREEN frames. +func (h *Hub) ScreenDeviceID(ctx *connection.Context) string { + h.screenIndexMu.RLock() + defer h.screenIndexMu.RUnlock() + return h.screenIndex[ctx] +} + +// UnbindScreenConn removes the screen sub-connection mapping (called on TCP +// disconnect of a screen sub-context). No-op if the context isn't tracked. +func (h *Hub) UnbindScreenConn(ctx *connection.Context) { + h.screenIndexMu.Lock() + deviceID, ok := h.screenIndex[ctx] + if !ok { + h.screenIndexMu.Unlock() + return + } + delete(h.screenIndex, ctx) + h.screenIndexMu.Unlock() + + h.mu.Lock() + if d, ok := h.devices[deviceID]; ok && d.screenConn == ctx { + d.screenConn = nil + // Clear the cache too — when this device's screen comes back up, the + // resolution and IDR will be republished fresh. + d.screenWidth = 0 + d.screenHeight = 0 + d.lastKeyframe = nil + } + h.mu.Unlock() } // Subscribe registers an EventHandler. The returned func removes it. @@ -119,14 +287,16 @@ func (h *Hub) snapshotSubscribers() []EventHandler { return out } -// Register records a device as online. Re-registering an existing ID overwrites -// the previous entry (e.g. a client reconnect with the same MasterID). -// A nil device or empty ID is silently ignored. +// Register records a device as online and pins the main TCP connection that +// will receive outbound commands via SendToDevice. Re-registering an existing +// ID overwrites the previous entry (e.g. a client reconnect with the same +// MasterID). A nil device, nil conn, or empty ID is silently ignored. // Subscribers are notified after the device is added. -func (h *Hub) Register(d *Device) { - if d == nil || d.ID == "" { +func (h *Hub) Register(d *Device, conn *connection.Context) { + if d == nil || d.ID == "" || conn == nil { return } + d.conn = conn h.mu.Lock() h.devices[d.ID] = d info := deviceToInfo(d) @@ -187,6 +357,135 @@ func deviceToInfo(d *Device) DeviceInfo { } } +// PublishCursor notifies subscribers when the device reports a new cursor +// index. Repeated identical indices are suppressed so the WS isn't spammed +// with per-frame cursor JSON. No-op for unknown devices. +func (h *Hub) PublishCursor(deviceID string, index byte) { + h.mu.Lock() + d, ok := h.devices[deviceID] + if !ok { + h.mu.Unlock() + return + } + if d.cursorSeen && d.lastCursorIndex == index { + h.mu.Unlock() + return + } + d.cursorSeen = true + d.lastCursorIndex = index + h.mu.Unlock() + for _, s := range h.snapshotSubscribers() { + s.OnCursorChange(deviceID, index) + } +} + +// CloseScreen tears down the active screen sub-connection for the device, +// if any. Used when the last viewer leaves so the device stops capturing. +// +// Cache (screenConn / screenWidth / lastKeyframe) is cleared SYNCHRONOUSLY +// here, not deferred to the eventual OnDisconnect → UnbindScreenConn path. +// Otherwise a new viewer arriving in the brief window between TCP close and +// the disconnect callback would see Active=true with stale dimensions/IDR +// and skip the COMMAND_SCREEN_SPY kick, leaving the page stuck on a "connected" +// status with no frames ever arriving. +func (h *Hub) CloseScreen(deviceID string) { + h.mu.Lock() + d, ok := h.devices[deviceID] + if !ok { + h.mu.Unlock() + return + } + sc := d.screenConn + d.screenConn = nil + d.screenWidth = 0 + d.screenHeight = 0 + d.lastKeyframe = nil + h.mu.Unlock() + if sc != nil { + // Drop the screenIndex entry SYNCHRONOUSLY so any in-flight frames + // still draining out of the device on this sub-conn (between our + // FIN and the device's clean-up) are silently dropped instead of + // being relayed to the freshly initialized browser decoder. Mixing + // frames from the old x264 SPS/PPS sequence with the new session's + // decoder produces the classic "every other quick reconnect goes + // black" symptom — old NAL units come in via the old ctx after we + // nulled d.screenConn but before OnDisconnect fires. + h.screenIndexMu.Lock() + delete(h.screenIndex, sc) + h.screenIndexMu.Unlock() + + // Tell the client to shut its screen pipeline down gracefully. + // Without this, the client's IOCPClient sees recv()==0 as a network + // blip and fires m_ReconnectFunc, which: + // 1. Reconnects the sub-conn (~100 ms) + // 2. Re-sends ConnAuthPacket (no BITMAPINFO!) + // 3. Keeps the capture thread alive for ~10 s holding DXGI handles + // 4. ConnAuth eventually times out, ScreenManager exits + // Net effect: a second viewer arriving within ~10 s of leaving lands + // in the dead window where the device is still capturing for the old + // (now unrouted) sub-conn — page sits on "Waiting for video". + // + // COMMAND_BYE is what the C++ server sends via + // CDialogBase::SayByeBye (server/2015Remote/IOCPServer.h:248) before + // it tears down a sub-conn for the same reason. Client-side handler: + // CScreenManager::OnReceive case COMMAND_BYE + // (client/ScreenManager.cpp:812) sets m_bIsWorking=FALSE and calls + // StopRunning() — the clean exit path that does NOT trigger reconnect. + if h.sender != nil { + _ = h.sender(sc, []byte{protocol.CommandBye}) + } + // Mirror the C++ flow (ScreenSpyDlg.cpp:842 — Sleep(500); CancelIO()). + // Give the device's read loop a moment to pull COMMAND_BYE off the + // wire before our FIN arrives; otherwise on a fast LAN the BYE byte + // can be coalesced with the FIN and the client's IOCPClient may + // observe recv()==0 first and trigger reconnect anyway. + // Run the close on a goroutine so the caller (web handler) isn't + // blocked for 500 ms. screenIndex is already cleared above, so + // in-flight frames during the grace window are silently dropped. + go func(c *connection.Context) { + time.Sleep(500 * time.Millisecond) + c.Close() + }(sc) + } +} + +// PublishResolution announces a new (or first-ever) screen geometry for a +// device. The browser uses width/height to initialize its WebCodecs decoder. +// The latest dimensions are also cached on the Device so future late-joining +// viewers can be bootstrapped without waiting for the next BITMAPINFO. +func (h *Hub) PublishResolution(deviceID string, width, height int) { + h.mu.Lock() + if d, ok := h.devices[deviceID]; ok { + d.screenWidth = width + d.screenHeight = height + } + h.mu.Unlock() + for _, s := range h.snapshotSubscribers() { + s.OnResolutionChange(deviceID, width, height) + } +} + +// PublishScreenFrame fans out a screen frame packet to all subscribers. +// Callers must have already wrapped the H.264 NAL payload in the +// [DeviceID:4][FrameType:1][DataLen:4][...] header expected by the browser. +// The packet slice is shared with subscribers — do not mutate after publish. +// +// Keyframe packets are also retained on the Device record so a new viewer +// joining a live session can immediately receive a decodable starting point +// instead of waiting up to ~15 s for the next IDR. +func (h *Hub) PublishScreenFrame(deviceID string, packet []byte, isKeyframe bool) { + if isKeyframe { + h.mu.Lock() + if d, ok := h.devices[deviceID]; ok { + d.lastKeyframe = packet + } + h.mu.Unlock() + } + for _, s := range h.snapshotSubscribers() { + s.OnScreenFrame(deviceID, packet, isKeyframe) + } +} + // UpdateLive applies a heartbeat-derived RTT and active-window title to the // device's live fields, then notifies subscribers. No-op if the device is // not registered (e.g. heartbeat arriving for a connection that never sent diff --git a/server/go/hub/hub_test.go b/server/go/hub/hub_test.go index 930cc4f..cbd8185 100644 --- a/server/go/hub/hub_test.go +++ b/server/go/hub/hub_test.go @@ -5,16 +5,22 @@ import ( "sync" "testing" "time" + + "github.com/yuanyuanxiang/SimpleRemoter/server/go/connection" ) +// stubCtx returns a non-nil *connection.Context useful only as a sentinel. +// Tests never invoke Send / Close on it. +func stubCtx() *connection.Context { return &connection.Context{} } + func TestHubRegisterListUnregister(t *testing.T) { h := New() if got := h.Count(); got != 0 { t.Fatalf("empty hub: want Count=0, got %d", got) } - h.Register(&Device{ID: "a", Name: "Alice", ConnectedAt: time.Now()}) - h.Register(&Device{ID: "b", Name: "Bob", ConnectedAt: time.Now()}) + h.Register(&Device{ID: "a", Name: "Alice", ConnectedAt: time.Now()}, stubCtx()) + h.Register(&Device{ID: "b", Name: "Bob", ConnectedAt: time.Now()}, stubCtx()) if got := h.Count(); got != 2 { t.Fatalf("after 2 registers: want Count=2, got %d", got) } @@ -38,8 +44,9 @@ func TestHubRegisterListUnregister(t *testing.T) { func TestHubNilAndEmptyIgnored(t *testing.T) { h := New() - h.Register(nil) - h.Register(&Device{ID: ""}) + h.Register(nil, stubCtx()) + h.Register(&Device{ID: ""}, stubCtx()) + h.Register(&Device{ID: "valid"}, nil) // nil conn should also be rejected h.Unregister("") if got := h.Count(); got != 0 { t.Fatalf("nil/empty register should be no-op, got Count=%d", got) @@ -71,13 +78,19 @@ func (c *captureHandler) OnDeviceUpdate(id string, rtt int, _ string) { c.mu.Unlock() } +func (c *captureHandler) OnScreenFrame(_ string, _ []byte, _ bool) {} + +func (c *captureHandler) OnResolutionChange(_ string, _, _ int) {} + +func (c *captureHandler) OnCursorChange(_ string, _ byte) {} + func TestHubSubscribeEvents(t *testing.T) { h := New() c := &captureHandler{} unsub := h.Subscribe(c) - h.Register(&Device{ID: "x", Name: "x"}) - h.Register(&Device{ID: "y", Name: "y"}) + h.Register(&Device{ID: "x", Name: "x"}, stubCtx()) + h.Register(&Device{ID: "y", Name: "y"}, stubCtx()) h.Unregister("x") h.Unregister("nonexistent") // no event @@ -89,7 +102,7 @@ func TestHubSubscribeEvents(t *testing.T) { } unsub() - h.Register(&Device{ID: "z"}) + h.Register(&Device{ID: "z"}, stubCtx()) if len(c.online) != 2 { t.Fatalf("after unsubscribe should not receive events: %+v", c.online) } @@ -100,7 +113,7 @@ func TestHubUpdateLive(t *testing.T) { c := &captureHandler{} h.Subscribe(c) - h.Register(&Device{ID: "x", Name: "x"}) + h.Register(&Device{ID: "x", Name: "x"}, stubCtx()) h.UpdateLive("x", 42, "Notepad") h.UpdateLive("ghost", 999, "should be ignored") // unknown id, no event @@ -116,8 +129,8 @@ func TestHubUpdateLive(t *testing.T) { func TestHubRegisterOverwrites(t *testing.T) { h := New() - h.Register(&Device{ID: "x", Name: "first"}) - h.Register(&Device{ID: "x", Name: "second"}) + h.Register(&Device{ID: "x", Name: "first"}, stubCtx()) + h.Register(&Device{ID: "x", Name: "second"}, stubCtx()) list := h.ListDevices() if len(list) != 1 || list[0].Name != "second" { t.Fatalf("re-register should overwrite, got %+v", list) @@ -137,7 +150,7 @@ func TestHubConcurrent(t *testing.T) { defer wg.Done() for i := range opsPer { id := fmt.Sprintf("g%d-%d", g, i) - h.Register(&Device{ID: id, Name: id, ConnectedAt: time.Now()}) + h.Register(&Device{ID: id, Name: id, ConnectedAt: time.Now()}, stubCtx()) _ = h.ListDevices() _ = h.Count() h.Unregister(id) diff --git a/server/go/protocol/commands.go b/server/go/protocol/commands.go index b5ea1c8..ca3f46a 100644 --- a/server/go/protocol/commands.go +++ b/server/go/protocol/commands.go @@ -2,7 +2,10 @@ package protocol import ( "bytes" + "crypto/hmac" + "crypto/sha256" "encoding/binary" + "encoding/hex" "strings" "golang.org/x/text/encoding/simplifiedchinese" @@ -44,19 +47,126 @@ func cleanString(s string) string { return strings.TrimSpace(result.String()) } -// Command tokens - matching the C++ definitions +// Command tokens - matching the C++ definitions (common/commands.h). const ( // Server -> Client commands - CommandActived byte = 0 // COMMAND_ACTIVED - CommandBye byte = 204 // COMMAND_BYE - disconnect - CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK + CommandActived byte = 0 // COMMAND_ACTIVED + CommandScreenSpy byte = 16 // COMMAND_SCREEN_SPY - start screen capture + CommandNext byte = 30 // COMMAND_NEXT - "control-side dialog is open, you may stream" + CommandBye byte = 204 // COMMAND_BYE - disconnect + CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK // Client -> Server tokens - TokenAuth byte = 100 // TOKEN_AUTH - authorization required - TokenHeartbeat byte = 101 // TOKEN_HEARTBEAT - TokenLogin byte = 102 // TOKEN_LOGIN - login packet + TokenAuth byte = 100 // TOKEN_AUTH - authorization required + TokenHeartbeat byte = 101 // TOKEN_HEARTBEAT + TokenLogin byte = 102 // TOKEN_LOGIN - login packet + TokenBitmapInfo byte = 115 // TOKEN_BITMAPINFO - screen sub-connection header + TokenFirstScreen byte = 116 // TOKEN_FIRSTSCREEN - raw BGRA baseline frame (NOT H264) + TokenNextScreen byte = 117 // TOKEN_NEXTSCREEN - non-keyframe H264 (P-frame) + TokenKeyframe byte = 134 // TOKEN_KEYFRAME - H264 IDR (sent on GOP boundary) + TokenConnAuth byte = 246 // TOKEN_CONN_AUTH - sub-connection identity handshake + CmdCursorImage byte = 93 // CMD_CURSOR_IMAGE - custom cursor bitmap (Phase 5+ feature) ) +// Sub-connection authentication (matches common/commands.h ConnAuth* structs). +// Each newly-opened sub-conn first sends a 512-byte ConnAuthPacket, then waits +// for a 256-byte ConnAuthAck before any further command is meaningful. +const ( + ConnAuthPacketSize = 512 + ConnAuthAckSize = 256 + // ConnAuthAck field offsets within the 256-byte buffer. + ConnAuthAckOffStatus = 1 // uint8 + ConnAuthAckOffServerTime = 2 // uint64 LE + // Status codes. + ConnAuthStatusOK byte = 0 +) + +// CMD_MASTERSETTING is the server's reply to a fresh client login. The +// client uses the Signature field to prove this server has the shared +// secret; without a valid signature the client's private FileUpload init +// aborts the process. Struct layout matches MasterSettings in +// common/commands.h (pragma pack 4, total 1000 bytes). +const ( + CmdMasterSetting byte = 215 + MasterSettingsSize = 1000 + MasterSettingsOffReportInterval = 0 // int32, seconds + MasterSettingsOffSignature = 508 // Signature[64] + MasterSettingsSignatureLen = 64 + // DefaultReportIntervalSec matches the C++ default. Sending 0 makes the + // client disable its active-window heartbeat field, breaking RTT / + // ActiveWindow live updates on the web UI. + DefaultReportIntervalSec = 5 +) + +// SignMessage computes HMAC-SHA256(key, msg) and returns the 64-char +// lowercase hex digest. Used to sign CMD_MASTERSETTING replies so the +// client can verify the response came from a legitimate server. +// +// The key is a deployment-time shared secret loaded from the +// YAMA_SIGN_PASSWORD env var so the binary doesn't carry the literal in +// cleartext; provision out-of-band and never commit it. +func SignMessage(password string, msg []byte) string { + mac := hmac.New(sha256.New, []byte(password)) + mac.Write(msg) + return hex.EncodeToString(mac.Sum(nil)) +} + +// Screen-spy parameters that match the C++ ScreenSpy implementation. +const ( + AlgorithmH264 byte = 2 // ALGORITHM_H264 — H264 encoding (the algorithm web uses) +) + +// Reserved-field indices we care about (see common/commands.h RES_* enum). +// LOGIN_INFOR.szReserved is a '|'-separated list; clients fill known slots +// even when leaving others blank ("?"). +const ( + ResFieldClientType = 0 // RES_CLIENT_TYPE — client kind (Windows / macOS / ...) + ResFieldFilePath = 4 // RES_FILE_PATH — install path + ResFieldInstallTime = 6 // RES_INSTALL_TIME + ResFieldClientLoc = 10 // RES_CLIENT_LOC — geo string + ResFieldClientPubIP = 11 // RES_CLIENT_PUBIP — public IP + ResFieldClientID = 16 // RES_CLIENT_ID — uint64 decimal, matches TOKEN_BITMAPINFO clientID +) + +// ScreenFrameHeaderLen is the size of the small per-frame header prepended by +// the device on every TOKEN_NEXTSCREEN buffer, before the H.264 NAL payload. +// Layout (excluding the leading TOKEN_* byte): +// +// [algorithm:1][cursorPos:8 (int32 x, int32 y)][cursorIdx:1] = 10 bytes +// +// (The C++ side counts the token byte into its ulHeadLength=11; we keep the +// constant strictly post-token so the call site reads `skip := 1 + headerLen` +// without confusion.) SCREENYSPY_IMPROVE adds a 4-byte frameID after the +// cursor index, which is the production-off setting per common/commands.h. +const ScreenFrameHeaderLen = 1 + 8 + 1 + +// IsH264Keyframe scans an Annex-B H.264 bitstream for a NAL unit indicating +// a keyframe boundary — IDR (type 5), SPS (7) or PPS (8). Returns true on +// the first hit. Matches the detection used by the C++ ScreenSpy broadcast +// path so frame-type bytes stay consistent across server implementations. +func IsH264Keyframe(data []byte) bool { + n := len(data) + for i := 0; i+4 < n; i++ { + var nalOffset int + switch { + case data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1: + nalOffset = i + 4 + case data[i] == 0 && data[i+1] == 0 && data[i+2] == 1: + nalOffset = i + 3 + default: + continue + } + if nalOffset >= n { + continue + } + nalType := data[nalOffset] & 0x1F + if nalType == 5 || nalType == 7 || nalType == 8 { + return true + } + } + return false +} + // LOGIN_INFOR structure size and offsets (matching C++ struct with default alignment) // Note: C++ struct uses default alignment (4-byte for uint32/int) const ( diff --git a/server/go/protocol/commands_test.go b/server/go/protocol/commands_test.go new file mode 100644 index 0000000..1fd9e69 --- /dev/null +++ b/server/go/protocol/commands_test.go @@ -0,0 +1,47 @@ +package protocol + +import "testing" + +func TestSignMessageHMACVector(t *testing.T) { + // Standard HMAC-SHA256 sanity vector. Anchors that SignMessage matches + // the canonical RFC 4231 algorithm so signatures stay interoperable + // with peers that compute the same digest. + got := SignMessage("key", []byte("hello")) + want := "9307b3b915efb5171ff14d8cb55fbcc798c6c0ef1456d66ded1a6aa723a58b7b" + if got != want { + t.Fatalf("SignMessage(key, hello) = %s, want %s", got, want) + } +} + +func TestSignMessageDeterministic(t *testing.T) { + a := SignMessage("test-key", []byte("2026-01-01 12:00:00|123456789")) + b := SignMessage("test-key", []byte("2026-01-01 12:00:00|123456789")) + if a != b { + t.Fatalf("non-deterministic: %s != %s", a, b) + } + if len(a) != 64 { + t.Fatalf("expected 64 hex chars, got %d (%s)", len(a), a) + } +} + +func TestIsH264KeyframeBasic(t *testing.T) { + // 4-byte start code + IDR (NAL type 5) + idr := []byte{0x00, 0x00, 0x00, 0x01, 0x65, 0x88} + if !IsH264Keyframe(idr) { + t.Fatal("IDR should be detected as keyframe") + } + // 3-byte start code + SPS (NAL type 7) + sps := []byte{0x00, 0x00, 0x01, 0x67, 0x42} + if !IsH264Keyframe(sps) { + t.Fatal("SPS should be detected as keyframe") + } + // 4-byte start code + non-IDR slice (NAL type 1) + pframe := []byte{0x00, 0x00, 0x00, 0x01, 0x41, 0x9b} + if IsH264Keyframe(pframe) { + t.Fatal("non-IDR slice should not be detected as keyframe") + } + // Garbage + if IsH264Keyframe([]byte{0xde, 0xad, 0xbe, 0xef}) { + t.Fatal("non-H264 bytes should not match") + } +} diff --git a/server/go/protocol/parser.go b/server/go/protocol/parser.go index 7a4868b..9daf901 100644 --- a/server/go/protocol/parser.go +++ b/server/go/protocol/parser.go @@ -20,6 +20,19 @@ type Parser struct { codec *Codec } +// findHTTPBodyOffset returns the byte offset of the HTTP body — i.e. one past +// the first `\r\n\r\n` separator. Returns -1 if the separator isn't present +// yet (caller should wait for more data). Matches the C++ UnMaskHttp scan in +// common/mask.h. +func findHTTPBodyOffset(data []byte) int { + for i := 0; i+4 <= len(data); i++ { + if data[i] == '\r' && data[i+1] == '\n' && data[i+2] == '\r' && data[i+3] == '\n' { + return i + 4 + } + } + return -1 +} + // NewParser creates a new parser func NewParser() *Parser { return &Parser{ @@ -38,6 +51,22 @@ func (p *Parser) Close() { func (p *Parser) Parse(ctx *connection.Context) ([]byte, error) { buf := ctx.InBuffer + // Strip optional HTTP-mask wrapper. The client may disguise each outbound + // chunk as a `POST / HTTP/1.1\r\n...\r\n\r\n` envelope followed + // by the real binary body (see common/mask.h: HttpMask). Each chunk + // carries its own envelope so we strip every time we see the prefix. + if buf.Len() >= 5 { + head := buf.Peek(5) + if len(head) == 5 && head[0] == 'P' && head[1] == 'O' && head[2] == 'S' && head[3] == 'T' && head[4] == ' ' { + bodyOffset := findHTTPBodyOffset(buf.Bytes()) + if bodyOffset < 0 { + // Headers not fully arrived yet — wait for more bytes. + return nil, ErrNeedMore + } + buf.Skip(bodyOffset) + } + } + // Need at least minimum bytes to determine protocol if buf.Len() < MinComLen { return nil, ErrNeedMore diff --git a/server/go/web/ws.go b/server/go/web/ws.go index 9af98c2..13d07cb 100644 --- a/server/go/web/ws.go +++ b/server/go/web/ws.go @@ -31,27 +31,46 @@ var upgrader = websocket.Upgrader{ // ----- per-connection client state ---------------------------------------- +// wsMsg is one queued WebSocket frame. binary toggles between +// websocket.TextMessage (JSON signaling) and websocket.BinaryMessage +// (screen frames). +type wsMsg struct { + binary bool + data []byte +} + type wsClient struct { conn *websocket.Conn - send chan []byte + send chan wsMsg closed chan struct{} once sync.Once // Mutated under wsHub.mu (or only by the read loop owning this client). - nonce string // outstanding challenge — cleared after a successful login - token string // set once authenticated - role string // mirrors session role after login - addr string // client address for logs + nonce string // outstanding challenge — cleared after a successful login + token string // set once authenticated + role string // mirrors session role after login + addr string // client address for logs + watching string // device ID this browser is currently streaming, "" when on the list } -// queue writes a payload onto the send buffer. Drops silently if the buffer -// is full so a stuck reader can't back-pressure the broadcast path. +// queue writes a JSON text frame onto the send buffer. Drops silently if the +// buffer is full so a stuck reader can't back-pressure the broadcast path. func (c *wsClient) queue(payload []byte) { + c.enqueue(wsMsg{binary: false, data: payload}) +} + +// queueBinary writes a binary WS frame. Used for screen-stream packets. +func (c *wsClient) queueBinary(payload []byte) { + c.enqueue(wsMsg{binary: true, data: payload}) +} + +func (c *wsClient) enqueue(m wsMsg) { select { - case c.send <- payload: + case c.send <- m: case <-c.closed: default: - // queue full — caller is responsible for noticing if it matters. + // queue full — drop (acceptable for video; signaling clients are + // typically not behind enough for the small text buffer to fill). } } @@ -105,6 +124,58 @@ func (h *wsHub) OnDeviceOffline(_ string) { h.broadcastAuthenticated(`{"cmd":"devices_changed"}`) } +// OnCursorChange relays the remote cursor index to every viewer of this +// device. The browser maps the index to a CSS cursor (desktop) or overlay +// SVG variant (touch). Hub already de-duplicates so we always have a real +// transition here. +func (h *wsHub) OnCursorChange(deviceID string, index byte) { + msg := mustJSON(map[string]any{ + "cmd": "cursor", + "index": index, + }) + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.watching == deviceID && c.token != "" { + c.queue(msg) + } + } +} + +// OnResolutionChange notifies viewers so the browser-side WebCodecs decoder +// can be (re)initialized with the right frame size. Without this, incoming +// binary frames after connect_result are decoded by an uninitialized +// VideoDecoder and the page stays on "Waiting for video...". +func (h *wsHub) OnResolutionChange(deviceID string, width, height int) { + msg := mustJSON(map[string]any{ + "cmd": "resolution_changed", + "id": deviceID, + "width": width, + "height": height, + }) + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.watching == deviceID && c.token != "" { + c.queue(msg) + } + } +} + +// OnScreenFrame ships a screen packet to every browser currently watching +// this device. We hold the read lock for the whole iteration, but each +// queueBinary is non-blocking (drops on backpressure) so a slow viewer +// cannot stall the fast ones. +func (h *wsHub) OnScreenFrame(deviceID string, packet []byte, _ bool) { + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.watching == deviceID && c.token != "" { + c.queueBinary(packet) + } + } +} + // OnDeviceUpdate forwards heartbeat-derived liveness data so the device-list // rows can refresh RTT and active-window labels without re-fetching. func (h *wsHub) OnDeviceUpdate(id string, rtt int, activeWindow string) { @@ -144,6 +215,12 @@ func (h *wsHub) unregister(c *wsClient) { h.mu.Lock() delete(h.clients, c) h.mu.Unlock() + // If this client was the last viewer of a device, tear down the screen + // session so the device stops encoding. Done OUTSIDE the lock so the + // hub's mutators can take their own locks without risk of recursion. + if c.watching != "" && h.countWatchers(c.watching) == 0 { + h.devices.CloseScreen(c.watching) + } // Do NOT revoke the token: tokens are session-scoped, not WS-scoped. // Frontend may close+reopen the WS at any time (visibilitychange handler, // brief network blip, reload) and must be able to resume with the same @@ -170,7 +247,7 @@ func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) { client := &wsClient{ conn: conn, - send: make(chan []byte, wsSendBuffer), + send: make(chan wsMsg, wsSendBuffer), closed: make(chan struct{}), nonce: nonce, addr: r.RemoteAddr, @@ -192,8 +269,12 @@ func (h *wsHub) writeLoop(c *wsClient) { for { select { case msg := <-c.send: + msgType := websocket.TextMessage + if msg.binary { + msgType = websocket.BinaryMessage + } _ = c.conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) - if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { + if err := c.conn.WriteMessage(msgType, msg.data); err != nil { c.close() return } diff --git a/server/go/web/ws_handlers.go b/server/go/web/ws_handlers.go index 2921f43..45e0cdf 100644 --- a/server/go/web/ws_handlers.go +++ b/server/go/web/ws_handlers.go @@ -2,6 +2,9 @@ package web import ( "encoding/json" + + "github.com/yuanyuanxiang/SimpleRemoter/server/go/hub" + "github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol" ) // dispatch routes one inbound message to its handler. The `raw` payload is @@ -21,12 +24,10 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { case "ping": // no-op heartbeat; the read itself was the keep-alive signal case "disconnect": - c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`)) + h.handleDisconnect(c, raw) - // Reserved for later phases. Reply with a benign failure so the UI can - // surface a clear error instead of spinning indefinitely. case "connect": - h.replyNotImplemented(c, "connect_result", "Screen sharing not yet implemented on Go server") + h.handleConnect(c, raw) case "rdp_reset": // silently ignored — UI uses this as a fire-and-forget case "mouse", "key": @@ -116,6 +117,125 @@ func (h *wsHub) handleLogin(c *wsClient, raw []byte) { })) } +// handleConnect kicks off a screen-sharing session for the browser. We send +// COMMAND_SCREEN_SPY to the device's main TCP connection; the device then +// opens a new sub-connection (TOKEN_BITMAPINFO) which the TCP side binds to +// the device via hub.BindScreenConn. Frame relay to the browser is handled +// in Phase 4.2 once frames actually arrive. +// +// Reply semantics: returning connect_result.ok=true (without width/height) +// triggers the browser's "Waiting for video..." spinner. We can't deliver +// width/height here because we don't yet know them — they show up in the +// first TOKEN_BITMAPINFO from the device. +// handleDisconnect detaches this client from any device it was watching and +// — if no other authenticated client is still watching — closes the device's +// screen sub-connection. Closing the TCP sub-conn is the signal the C++ +// device firmware uses to stop screen capture, so this is how we ask the +// device to free its encoder. +func (h *wsHub) handleDisconnect(c *wsClient, _ []byte) { + // Mirror handleConnect: take h.mu so event-handler readers + // (OnResolutionChange/OnScreenFrame) get a consistent view of c.watching. + h.mu.Lock() + prev := c.watching + c.watching = "" + h.mu.Unlock() + c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`)) + if prev != "" && h.countWatchers(prev) == 0 { + h.devices.CloseScreen(prev) + } +} + +// countWatchers returns how many authenticated clients still have their +// `watching` field pointing at deviceID. Called from disconnect paths. +func (h *wsHub) countWatchers(deviceID string) int { + h.mu.RLock() + defer h.mu.RUnlock() + n := 0 + for c := range h.clients { + if c.watching == deviceID { + n++ + } + } + return n +} + +func (h *wsHub) handleConnect(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "connect_result") { + return + } + var in struct { + ID string `json:"id"` + } + if err := json.Unmarshal(raw, &in); err != nil || in.ID == "" { + c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": false, "msg": "Bad request"})) + return + } + + // If a screen session is already live for this device (another browser + // is already watching), reuse it: hand the new viewer the current + // resolution and the most recent IDR keyframe so its decoder can start + // rendering immediately, without waiting for the next IDR (~15 s). + cache := h.devices.ScreenState(in.ID) + if cache.Active { + c.queue(mustJSON(map[string]any{ + "cmd": "connect_result", "ok": true, + "width": cache.Width, "height": cache.Height, + })) + if len(cache.Keyframe) > 0 { + c.queueBinary(cache.Keyframe) + } + h.mu.Lock() + c.watching = in.ID + h.mu.Unlock() + return + } + + // No active session — kick the device to start capturing. We send the + // same 32-byte COMMAND_SCREEN_SPY payload the C++ WebService sends: + // [0]=COMMAND_SCREEN_SPY, [1]=0 (GDI), [2]=ALGORITHM_H264, [3]=1 (multi-screen), + // [4..31]=0. + cmd := make([]byte, 32) + cmd[0] = protocol.CommandScreenSpy + cmd[2] = protocol.AlgorithmH264 + cmd[3] = 1 + + // CRITICAL: bind c.watching BEFORE asking the device to start capturing. + // On fast reconnects the device's screen sub-conn handshake completes in + // <100 ms, so TOKEN_BITMAPINFO and even the first H264 frame can arrive + // before this handler finishes — and the resolution_changed / frame + // broadcasts in wsHub filter on c.watching. With the assignment after + // SendToDevice the new viewer silently misses the very first IDR and + // resolution_changed, leaving the page stuck on "Waiting for video". + // + // The write needs to share the lock event handlers use to read c.watching + // (they iterate h.clients under h.mu.RLock). Without that the write is a + // data race; on a fast reconnect the reader goroutine can keep observing + // the previous value ("") long enough to drop the first resolution_changed + // and the first IDR, which produces the exact "every other quick reconnect + // goes black" symptom — the C++ server avoids it because it does the same + // state mutation under std::mutex and reaps the memory-barrier as a bonus. + h.mu.Lock() + c.watching = in.ID + h.mu.Unlock() + + if err := h.devices.SendToDevice(in.ID, cmd); err != nil { + // Roll back the watching flag if we never managed to kick capture. + h.mu.Lock() + c.watching = "" + h.mu.Unlock() + msg := "Device offline" + if err != hub.ErrDeviceOffline { + msg = "Failed to start screen capture" + h.log.Error("SendToDevice(%s): %v", in.ID, err) + } + c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": false, "msg": msg})) + return + } + h.log.Info("[timing] COMMAND_SCREEN_SPY sent to device=%s (cold start)", in.ID) + + c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": true})) +} + func (h *wsHub) handleGetDevices(c *wsClient, raw []byte) { if !h.requireAuth(c, raw, "device_list") { return -- 2.43.0 From 98a914f963ff1bd034345302adae84b32cb3f7c6 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 18 May 2026 13:53:44 +0200 Subject: [PATCH 5/7] Feature(Go): Mouse/keyboard input + user management with users.json (Phase 5 + 7) --- .gitignore | 1 + server/go/README.md | 1 + server/go/cmd/main.go | 32 +++- server/go/hub/hub.go | 39 +++++ server/go/protocol/commands.go | 172 +++++++++++++++++-- server/go/web/ws_handlers.go | 304 ++++++++++++++++++++++++++++++++- server/go/wsauth/wsauth.go | 236 +++++++++++++++++++++++++ 7 files changed, 764 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 7ee8739..0cd6d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ YAMA.code-workspace Bin/* nul server/go/web/assets/index.html +server/go/users.json diff --git a/server/go/README.md b/server/go/README.md index 96cdde5..27c6be1 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -120,6 +120,7 @@ VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。 | `YAMA_PWD` | 超级密码,用于 HMAC 签名验证;也作为 Web admin 密码的默认来源 | `your_super_password` | | `YAMA_WEB_ADMIN_PASS` | Web UI 的 admin 密码(明文);优先于 `YAMA_PWD`。两者都未设置时 Web 登录禁用 | `your_admin_password` | | `YAMA_SIGN_PASSWORD` | HMAC-SHA256 key used to sign CMD_MASTERSETTING replies; must match the client's expected value. Provision out-of-band. Unset → client refuses screen/file ops. | `` | +| `YAMA_USERS_FILE` | Path to the JSON file that persists non-admin web users (allowed_groups, password hash, salt). Default is `users.json` in the working directory. | `users.json` | ```bash # Linux/macOS diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index 364f9f3..cbda53d 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -286,6 +286,13 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { if len(reserved) > protocol.ResFieldClientLoc { location = info.GetReservedField(protocol.ResFieldClientLoc) } + // RES_RESOLUTION is already formatted by the client as "N:W*H" + // (see client/LoginServer.cpp:414). Pass through unchanged so the web + // UI's device card renders it next to the version label. + resolution := "" + if len(reserved) > protocol.ResFieldResolution { + resolution = info.GetReservedField(protocol.ResFieldResolution) + } // Register with hub so the web side can list this device. Sub-connections // (screen / terminal etc.) reuse the MasterID and will overwrite this entry @@ -301,6 +308,7 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) { FilePath: clientInfo.FilePath, InstallTime: info.StartTime, Location: location, + Resolution: resolution, PeerIP: ctx.GetPeerIP(), PublicIP: clientInfo.IP, ConnectedAt: time.Now(), @@ -399,9 +407,18 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) { if info := ctx.GetInfo(); info.ClientID != "" { var rtt int32 var activeWindow string - // ActiveWnd at data[9..521] is a 512-byte GBK-encoded string. + // ActiveWnd at data[9..521] is a 512-byte NUL-padded string. Encoding + // depends on the client: new clients advertise CLIENT_CAP_UTF8 (bit + // 0x0002 in the moduleVersion hex tail) and ship UTF-8 directly; + // legacy Windows clients still use CP936 (GBK). Decoding UTF-8 bytes + // as GBK turns Chinese characters into mojibake — see the matching + // C++ guard at server/2015Remote/WebService.cpp:1530. if len(data) >= 9+512 { - activeWindow = protocol.GbkToUTF8(data[9 : 9+512]) + activeWindow = protocol.DecodeClientString( + data[9:9+512], + h.hub.Capability(info.ClientID), + info.ClientType, + ) } // Ping at data[521..525] is a little-endian int32. if len(data) >= 525 { @@ -551,6 +568,17 @@ func main() { log.Warn("Neither YAMA_WEB_ADMIN_PASS nor YAMA_PWD is set; web login will be unavailable") } + // Persistent users live in users.json next to the binary's working dir + // — same default the C++ WebService uses (m_ConfigDir + "users.json"). + // Loading is best-effort: a missing file means "no extra users yet". + usersFile := os.Getenv("YAMA_USERS_FILE") + if usersFile == "" { + usersFile = "users.json" + } + if err := webAuth.SetUsersFile(usersFile); err != nil { + log.Warn("Failed to load %s: %v (continuing with admin only)", usersFile, err) + } + // Create servers for each port var servers []*server.Server for _, port := range ports { diff --git a/server/go/hub/hub.go b/server/go/hub/hub.go index 51c6670..f600fd6 100644 --- a/server/go/hub/hub.go +++ b/server/go/hub/hub.go @@ -48,6 +48,7 @@ type Device struct { Location string // client-reported geo string (reserved field 10) PeerIP string // network-level remote address as seen by the server PublicIP string // client-reported public IP (reserved field 11) + Resolution string // client-formatted screen geometry "N:W*H" (reserved field 15) ConnectedAt time.Time // Live fields refreshed on every heartbeat. Protected by hub.mu. @@ -115,6 +116,18 @@ func (h *Hub) MainConn(id string) *connection.Context { return nil } +// Capability returns the device's reported capability hex string +// (LOGIN_INFOR.moduleVersion tail). Empty for unknown devices — callers +// should treat that as "no caps" (legacy Windows GBK default). +func (h *Hub) Capability(id string) string { + h.mu.RLock() + defer h.mu.RUnlock() + if d, ok := h.devices[id]; ok { + return d.Capability + } + return "" +} + // DeviceInfo is the JSON-safe projection of Device for the /api/devices // endpoint and the WS device_list message. Field names match what the // existing browser front-end expects. @@ -131,6 +144,7 @@ type DeviceInfo struct { Location string `json:"location,omitempty"` IP string `json:"ip"` // client-reported public IP (matches C++ key) PeerIP string `json:"peer_ip,omitempty"` + Screen string `json:"screen,omitempty"` // "N:W*H" — matches C++ DeviceInfo.screen key RTT int `json:"rtt"` ActiveWindow string `json:"activeWindow,omitempty"` ConnectedAt int64 `json:"connected_at"` @@ -205,6 +219,30 @@ func (h *Hub) SendToDevice(id string, data []byte) error { return h.sender(d.conn, data) } +// SendToScreen routes a payload to the device's currently-bound screen +// sub-connection. Input events (COMMAND_SCREEN_CONTROL) MUST go through the +// screen sub-conn rather than the main conn — the C++ client only dispatches +// these commands from CScreenManager::OnReceive, which reads exclusively from +// the sub-conn (see client/ScreenManager.cpp:1065). Returns ErrDeviceOffline +// when the device is unknown OR has no active screen session, so callers can +// quietly drop input from browsers that haven't called connect yet. +func (h *Hub) SendToScreen(id string, data []byte) error { + h.mu.RLock() + d, ok := h.devices[id] + var sc *connection.Context + if ok { + sc = d.screenConn + } + h.mu.RUnlock() + if !ok || sc == nil { + return ErrDeviceOffline + } + if h.sender == nil { + return ErrNoSender + } + return h.sender(sc, data) +} + // BindScreenConn associates a freshly-arrived sub-connection (the one that // just sent TOKEN_BITMAPINFO) with the device identified by clientID. // Returns false if the device is not registered — callers should drop the @@ -350,6 +388,7 @@ func deviceToInfo(d *Device) DeviceInfo { Location: d.Location, IP: d.PublicIP, PeerIP: d.PeerIP, + Screen: d.Resolution, RTT: d.RTT, ActiveWindow: d.ActiveWindow, ConnectedAt: d.ConnectedAt.Unix(), diff --git a/server/go/protocol/commands.go b/server/go/protocol/commands.go index ca3f46a..09fe249 100644 --- a/server/go/protocol/commands.go +++ b/server/go/protocol/commands.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "strconv" "strings" "golang.org/x/text/encoding/simplifiedchinese" @@ -36,6 +37,21 @@ func GbkToUTF8(data []byte) string { return cleanString(buf.String()) } +// Utf8CleanString trims at the first NUL and strips non-printables — the +// UTF-8 counterpart of GbkToUTF8 for clients that have the CLIENT_CAP_UTF8 +// capability bit. Decoding as GBK in that case would mangle multi-byte +// sequences (the C++ comment at WebService.cpp:1530 calls out this exact +// "double-encoding" footgun). +func Utf8CleanString(data []byte) string { + if idx := bytes.IndexByte(data, 0); idx >= 0 { + data = data[:idx] + } + if len(data) == 0 { + return "" + } + return cleanString(string(data)) +} + // cleanString removes non-printable characters except common whitespace func cleanString(s string) string { var result strings.Builder @@ -47,14 +63,52 @@ func cleanString(s string) string { return strings.TrimSpace(result.String()) } +// Client capability bitmask values, matching common/commands.h CLIENT_CAP_*. +// Reported in the hex tail of LOGIN_INFOR.moduleVersion (after the '-'). +const ( + ClientCapV2 uint32 = 0x0001 // CLIENT_CAP_V2 — V2 file transfer + ClientCapUTF8 uint32 = 0x0002 // CLIENT_CAP_UTF8 — UTF-8 protocol strings (activeWindow, key-log titles, ...) + ClientCapScreenPreview uint32 = 0x0004 // CLIENT_CAP_SCREEN_PREVIEW +) + +// SupportsCap returns true when the client's reported capability hex string +// has the given bit set. An empty / unparseable string means "no caps" and +// matches the legacy GBK-Windows convention. +func SupportsCap(capability string, bit uint32) bool { + if capability == "" { + return false + } + caps, err := strconv.ParseUint(strings.TrimSpace(capability), 16, 32) + if err != nil { + return false + } + return uint32(caps)&bit != 0 +} + +// DecodeClientString decodes a fixed-length, NUL-padded buffer the client +// sent as part of a binary protocol field (typically ActiveWnd). If the +// client signals UTF-8 capability or is known to ship UTF-8 by default +// (Linux / macOS), the bytes are treated as UTF-8; otherwise they're +// decoded from GBK (CP936 — the legacy Windows default). +// +// clientType comes from LOGIN_INFOR reserved field 0 (RES_CLIENT_TYPE) and +// capability from the hex tail of moduleVersion. Both can be empty. +func DecodeClientString(data []byte, capability, clientType string) string { + if SupportsCap(capability, ClientCapUTF8) || clientType == "LNX" || clientType == "MAC" { + return Utf8CleanString(data) + } + return GbkToUTF8(data) +} + // Command tokens - matching the C++ definitions (common/commands.h). const ( // Server -> Client commands - CommandActived byte = 0 // COMMAND_ACTIVED - CommandScreenSpy byte = 16 // COMMAND_SCREEN_SPY - start screen capture - CommandNext byte = 30 // COMMAND_NEXT - "control-side dialog is open, you may stream" - CommandBye byte = 204 // COMMAND_BYE - disconnect - CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK + CommandActived byte = 0 // COMMAND_ACTIVED + CommandScreenSpy byte = 16 // COMMAND_SCREEN_SPY - start screen capture + CommandScreenControl byte = 20 // COMMAND_SCREEN_CONTROL - mouse/keyboard input (MSG64 batches) + CommandNext byte = 30 // COMMAND_NEXT - "control-side dialog is open, you may stream" + CommandBye byte = 204 // COMMAND_BYE - disconnect + CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK // Client -> Server tokens TokenAuth byte = 100 // TOKEN_AUTH - authorization required @@ -87,11 +141,11 @@ const ( // aborts the process. Struct layout matches MasterSettings in // common/commands.h (pragma pack 4, total 1000 bytes). const ( - CmdMasterSetting byte = 215 - MasterSettingsSize = 1000 + CmdMasterSetting byte = 215 + MasterSettingsSize = 1000 MasterSettingsOffReportInterval = 0 // int32, seconds - MasterSettingsOffSignature = 508 // Signature[64] - MasterSettingsSignatureLen = 64 + MasterSettingsOffSignature = 508 // Signature[64] + MasterSettingsSignatureLen = 64 // DefaultReportIntervalSec matches the C++ default. Sending 0 makes the // client disable its active-window heartbeat field, breaking RTT / // ActiveWindow live updates on the web UI. @@ -116,6 +170,101 @@ const ( AlgorithmH264 byte = 2 // ALGORITHM_H264 — H264 encoding (the algorithm web uses) ) +// Windows message constants used inside MSG64.message. The client dispatches +// on these values verbatim (CScreenManager::ProcessCommand at +// client/ScreenManager.cpp:1617), so these MUST stay bit-identical to the +// WinUser.h definitions even though this Go server is cross-platform. +const ( + WMKeyDown uint64 = 0x0100 + WMKeyUp uint64 = 0x0101 + WMSysKeyDown uint64 = 0x0104 + WMSysKeyUp uint64 = 0x0105 + WMMouseMove uint64 = 0x0200 + WMLButtonDown uint64 = 0x0201 + WMLButtonUp uint64 = 0x0202 + WMLButtonDblClk uint64 = 0x0203 + WMRButtonDown uint64 = 0x0204 + WMRButtonUp uint64 = 0x0205 + WMRButtonDblClk uint64 = 0x0206 + WMMButtonDown uint64 = 0x0207 + WMMButtonUp uint64 = 0x0208 + WMMouseWheel uint64 = 0x020A +) + +// Virtual-key codes referenced from the input mapping. Same numeric values +// as the Win32 VK_* constants. +const ( + VKLWin = 0x5B // VK_LWIN — filtered: never forwarded + VKRWin = 0x5C // VK_RWIN — filtered: never forwarded + VKPrior = 0x21 // VK_PRIOR (Page Up) — extended-key range start + VKDown = 0x28 // VK_DOWN — extended-key range end + VKInsert = 0x2D + VKDelete = 0x2E + VKNumLock = 0x90 + VKRControl = 0xA3 + VKRMenu = 0xA5 + VKApps = 0x5D +) + +// MK_* wParam bitflags for mouse-button messages. +const ( + MKLButton uint64 = 0x0001 + MKRButton uint64 = 0x0002 + MKMButton uint64 = 0x0010 +) + +// MSG64 is the 48-byte fixed layout the client expects inside a +// COMMAND_SCREEN_CONTROL packet (common/commands.h class MSG64). +// +// [hwnd:8][message:8][wParam:8][lParam:8][time:8][pt.x:4][pt.y:4] +// +// All uint64 fields are little-endian; pt is two int32 LE. The client's +// ProcessCommand validates `ulLength % 48 == 0` and treats each 48-byte +// block as one MSG64. +const Msg64Size = 48 + +// BuildScreenControlPacket encodes one COMMAND_SCREEN_CONTROL packet +// carrying a single MSG64 record. The cmd byte is prepended. +// +// Wire layout: +// +// [CMD:1][hwnd:8 LE][message:8 LE][wParam:8 LE][lParam:8 LE][time:8 LE][pt.x:4 LE][pt.y:4 LE] +// +// time is filled with a monotonic-ish ms value (ms since Unix epoch trimmed +// to 32 bits) so the client's GetTickCount() comparisons stay reasonable. +func BuildScreenControlPacket(message, wParam, lParam uint64, ptX, ptY int32, timeMs uint32) []byte { + buf := make([]byte, 1+Msg64Size) + buf[0] = CommandScreenControl + // hwnd left zero — the client recomputes hWnd via WindowFromPoint. + binary.LittleEndian.PutUint64(buf[1+8:1+16], message) + binary.LittleEndian.PutUint64(buf[1+16:1+24], wParam) + binary.LittleEndian.PutUint64(buf[1+24:1+32], lParam) + binary.LittleEndian.PutUint64(buf[1+32:1+40], uint64(timeMs)) + binary.LittleEndian.PutUint32(buf[1+40:1+44], uint32(ptX)) + binary.LittleEndian.PutUint32(buf[1+44:1+48], uint32(ptY)) + return buf +} + +// MakeLParam packs x into the low word and y into the high word — the +// Windows MAKELPARAM macro the client expects in mouse-message lParams. +func MakeLParam(x, y int32) uint64 { + return uint64(uint32(x)&0xFFFF) | (uint64(uint32(y)&0xFFFF) << 16) +} + +// IsExtendedKey returns true when the given Win32 VK code should set the +// extended-key bit (bit 24) in a keyboard lParam. Matches the C++ +// HandleKey logic (server/2015Remote/WebService.cpp:944). +func IsExtendedKey(vk int) bool { + if vk >= VKPrior && vk <= VKDown { + return true + } + switch vk { + case VKInsert, VKDelete, VKNumLock, VKRControl, VKRMenu, VKApps: + return true + } + return false +} + // Reserved-field indices we care about (see common/commands.h RES_* enum). // LOGIN_INFOR.szReserved is a '|'-separated list; clients fill known slots // even when leaving others blank ("?"). @@ -125,6 +274,7 @@ const ( ResFieldInstallTime = 6 // RES_INSTALL_TIME ResFieldClientLoc = 10 // RES_CLIENT_LOC — geo string ResFieldClientPubIP = 11 // RES_CLIENT_PUBIP — public IP + ResFieldResolution = 15 // RES_RESOLUTION — client-formatted screen geometry: "N:W*H" ResFieldClientID = 16 // RES_CLIENT_ID — uint64 decimal, matches TOKEN_BITMAPINFO clientID ) @@ -173,8 +323,8 @@ const ( LoginInfoSize = 980 // Total size of LOGIN_INFOR struct (with alignment padding) // Field offsets (with alignment padding) - OffsetToken = 0 // 1 byte (unsigned char) - OffsetOsVerInfoEx = 1 // 156 bytes (char[156]) + OffsetToken = 0 // 1 byte (unsigned char) + OffsetOsVerInfoEx = 1 // 156 bytes (char[156]) // 3 bytes padding here to align dwCPUMHz to 4-byte boundary OffsetCPUMHz = 160 // 4 bytes (unsigned int) - aligned to 4 OffsetModuleVersion = 164 // 24 bytes (char[24]) diff --git a/server/go/web/ws_handlers.go b/server/go/web/ws_handlers.go index 45e0cdf..cb5906f 100644 --- a/server/go/web/ws_handlers.go +++ b/server/go/web/ws_handlers.go @@ -2,6 +2,8 @@ package web import ( "encoding/json" + "sort" + "time" "github.com/yuanyuanxiang/SimpleRemoter/server/go/hub" "github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol" @@ -11,8 +13,10 @@ import ( // passed through so handlers can re-parse to their own shape. // // Phase 3 implements: get_salt, login, get_devices, ping, disconnect. -// Phase 4/5/6 commands (connect, mouse, key, term_*, etc.) get a friendly -// "not yet implemented" reply so the browser UI doesn't hang silently. +// Phase 4 adds: connect, screen frame relay. +// Phase 5 adds: mouse, key (input forwarding to the device screen sub-conn). +// Phase 6/7 commands (term_*, user mgmt) get a friendly "not yet implemented" +// reply so the browser UI doesn't hang silently. func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { switch cmd { case "get_salt": @@ -30,8 +34,10 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { h.handleConnect(c, raw) case "rdp_reset": // silently ignored — UI uses this as a fire-and-forget - case "mouse", "key": - // silently ignored — no remote screen yet + case "mouse": + h.handleMouse(c, raw) + case "key": + h.handleKey(c, raw) case "term_open": h.replyNotImplemented(c, "term_closed", "Web terminal not yet implemented on Go server") case "term_input", "term_resize", "term_close": @@ -39,13 +45,13 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { // Admin operations (Phase 7). case "create_user": - h.replyNotImplemented(c, "create_user_result", "User management not yet implemented") + h.handleCreateUser(c, raw) case "delete_user": - h.replyNotImplemented(c, "delete_user_result", "User management not yet implemented") + h.handleDeleteUser(c, raw) case "list_users": - h.replyNotImplemented(c, "list_users_result", "User management not yet implemented") + h.handleListUsers(c, raw) case "get_groups": - c.queue([]byte(`{"cmd":"groups","ok":true,"groups":[]}`)) + h.handleGetGroups(c, raw) } } @@ -57,6 +63,23 @@ func (h *wsHub) replyNotImplemented(c *wsClient, replyCmd, msg string) { })) } +// requireAdmin combines token validation with a role=="admin" check. The +// reply on failure has the standard `{cmd, ok:false, msg}` shape so the +// front-end's generic toast handler can surface the reason. +func (h *wsHub) requireAdmin(c *wsClient, raw []byte, replyCmd string) (ok bool) { + if !h.requireAuth(c, raw, replyCmd) { + return false + } + // c.role is cached on first successful requireAuth; safe to read here. + if c.role != "admin" { + c.queue(mustJSON(map[string]any{ + "cmd": replyCmd, "ok": false, "msg": "Permission denied", + })) + return false + } + return true +} + // ----- handlers ------------------------------------------------------------ func (h *wsHub) handleGetSalt(c *wsClient, raw []byte) { @@ -275,6 +298,271 @@ func (h *wsHub) requireAuth(c *wsClient, raw []byte, replyCmd string) bool { return true } +// handleMouse forwards one mouse event from the browser to the device's +// screen sub-connection as a COMMAND_SCREEN_CONTROL packet carrying a +// single MSG64. Mirrors the C++ CWebService::HandleMouse path +// (server/2015Remote/WebService.cpp:773) so the client's +// CScreenManager::ProcessCommand sees identical bytes regardless of which +// server is serving the device. Unauthenticated and unknown event types +// drop silently — input is high-frequency, error replies would just spam +// the WS. +func (h *wsHub) handleMouse(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "mouse_result") { + return + } + var in struct { + Type string `json:"type"` + X int32 `json:"x"` + Y int32 `json:"y"` + Button int `json:"button"` + Delta int `json:"delta"` + } + if err := json.Unmarshal(raw, &in); err != nil { + return + } + deviceID := c.watching + if deviceID == "" { + return // browser hasn't picked a device yet + } + + var message, wParam uint64 + switch in.Type { + case "down": + switch in.Button { + case 0: + message, wParam = protocol.WMLButtonDown, protocol.MKLButton + case 1: + message, wParam = protocol.WMMButtonDown, protocol.MKMButton + case 2: + message, wParam = protocol.WMRButtonDown, protocol.MKRButton + default: + return + } + case "up": + switch in.Button { + case 0: + message = protocol.WMLButtonUp + case 1: + message = protocol.WMMButtonUp + case 2: + message = protocol.WMRButtonUp + default: + return + } + case "move": + message = protocol.WMMouseMove + case "wheel": + message = protocol.WMMouseWheel + // Windows expects ±120 per notch in HIWORD(wParam). Browsers report + // `deltaY` in pixels with sign flipped relative to Win32 conventions + // (positive deltaY = scroll down). Normalize to one notch per call, + // signed so up scrolls "up" on the remote. + var wheel int16 + switch { + case in.Delta > 0: + wheel = -120 + case in.Delta < 0: + wheel = 120 + } + wParam = uint64(uint16(wheel)) << 16 + case "dblclick": + // Windows synthesizes double-click from rapid down/up sequences, so + // the C++ server only forwards this for macOS clients. We don't + // currently track client OS on the Go side; drop the event — the + // down/up pair the browser already sent is sufficient on Windows. + return + default: + return + } + + lParam := protocol.MakeLParam(in.X, in.Y) + pkt := protocol.BuildScreenControlPacket(message, wParam, lParam, in.X, in.Y, tickMillis()) + _ = h.devices.SendToScreen(deviceID, pkt) +} + +// handleKey forwards one keyboard event to the device. lParam is built to +// match the Windows convention so applications relying on TranslateMessage +// (e.g. text input fields) behave correctly. Mirrors +// CWebService::HandleKey at server/2015Remote/WebService.cpp:878. +// +// Scan code: the C++ server resolves the VK -> scan code via the local +// MapVirtualKey. Go is cross-platform so we leave the scan-code bits zero; +// the client side ultimately delivers events via SendInput/keybd_event +// which accept VK-only input, and the extended-key bit (bit 24) is what +// actually matters for distinguishing numpad/arrow keys. +func (h *wsHub) handleKey(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "key_result") { + return + } + var in struct { + KeyCode int `json:"keyCode"` + Down bool `json:"down"` + Alt bool `json:"alt"` + } + if err := json.Unmarshal(raw, &in); err != nil { + return + } + if in.KeyCode == protocol.VKLWin || in.KeyCode == protocol.VKRWin { + return // Windows keys stay local; matches both C++ server and client + } + deviceID := c.watching + if deviceID == "" { + return + } + + var message uint64 + switch { + case in.Alt && in.Down: + message = protocol.WMSysKeyDown + case in.Alt && !in.Down: + message = protocol.WMSysKeyUp + case in.Down: + message = protocol.WMKeyDown + default: + message = protocol.WMKeyUp + } + + // lParam layout (Win32 keyboard message): + // bits 0..15: repeat count (= 1) + // bits 16..23: scan code (we leave zero — see handleKey doc) + // bit 24: extended-key flag (arrows, numpad, RCtrl, RAlt, ...) + // bit 29: context code (Alt held) + // bits 30..31: previous-key/transition flags (both set on key-up) + lParam := uint64(1) + if protocol.IsExtendedKey(in.KeyCode) { + lParam |= 1 << 24 + } + if in.Alt { + lParam |= 1 << 29 + } + if !in.Down { + lParam |= 3 << 30 + } + + wParam := uint64(uint32(in.KeyCode)) + pkt := protocol.BuildScreenControlPacket(message, wParam, lParam, 0, 0, tickMillis()) + _ = h.devices.SendToScreen(deviceID, pkt) +} + +// handleCreateUser provisions a new web account. Admin-only. Mirrors the +// C++ CWebService::HandleCreateUser semantics (WebService.cpp:1009): the +// password is hashed with a fresh salt, the user table is persisted, and +// any duplicate username is rejected. +func (h *wsHub) handleCreateUser(c *wsClient, raw []byte) { + const replyCmd = "create_user_result" + if !h.requireAdmin(c, raw, replyCmd) { + return + } + var in struct { + Username string `json:"username"` + Password string `json:"password"` + Role string `json:"role"` + AllowedGroups []string `json:"allowed_groups"` + } + if err := json.Unmarshal(raw, &in); err != nil { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false, "msg": "Invalid JSON"})) + return + } + if in.Role == "" { + in.Role = "viewer" + } + if err := h.auth.CreateUser(in.Username, in.Password, in.Role, in.AllowedGroups); err != nil { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false, "msg": err.Error()})) + return + } + h.log.Info("user created by %s: name=%s role=%s groups=%d", + c.role, in.Username, in.Role, len(in.AllowedGroups)) + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": true})) +} + +// handleDeleteUser removes an account and revokes any of its live sessions. +// Admin-only. The bootstrap "admin" account is rejected at the wsauth layer. +func (h *wsHub) handleDeleteUser(c *wsClient, raw []byte) { + const replyCmd = "delete_user_result" + if !h.requireAdmin(c, raw, replyCmd) { + return + } + var in struct { + Username string `json:"username"` + } + if err := json.Unmarshal(raw, &in); err != nil { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false, "msg": "Invalid JSON"})) + return + } + if err := h.auth.DeleteUser(in.Username); err != nil { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false, "msg": err.Error()})) + return + } + h.log.Info("user deleted by %s: name=%s", c.role, in.Username) + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": true})) +} + +// handleListUsers returns the account table to admin callers. Field shape +// matches the C++ list_users_result the browser already parses: an array +// of {username, role, allowed_groups}. +func (h *wsHub) handleListUsers(c *wsClient, raw []byte) { + const replyCmd = "list_users_result" + if !h.requireAdmin(c, raw, replyCmd) { + return + } + users := h.auth.ListUsers() + out := make([]map[string]any, 0, len(users)) + for _, u := range users { + groups := u.AllowedGroups + if groups == nil { + groups = []string{} + } + out = append(out, map[string]any{ + "username": u.Username, + "role": u.Role, + "allowed_groups": groups, + }) + } + c.queue(mustJSON(map[string]any{ + "cmd": replyCmd, + "ok": true, + "users": out, + })) +} + +// handleGetGroups returns the deduplicated set of group labels currently +// observed on online devices, plus a synthetic "default" entry that the +// admin UI uses for ungrouped devices. Admin-only — non-admins don't get +// to enumerate the device fleet's groups. Matches the contract of +// CWebService::HandleGetGroups (WebService.cpp:1130). +func (h *wsHub) handleGetGroups(c *wsClient, raw []byte) { + const replyCmd = "groups" + if !h.requireAdmin(c, raw, replyCmd) { + return + } + seen := map[string]struct{}{"default": {}} + for _, d := range h.devices.ListDevices() { + g := d.Group + if g == "" { + g = "default" + } + seen[g] = struct{}{} + } + groups := make([]string, 0, len(seen)) + for g := range seen { + groups = append(groups, g) + } + sort.Strings(groups) + c.queue(mustJSON(map[string]any{ + "cmd": replyCmd, + "ok": true, + "groups": groups, + })) +} + +// tickMillis returns a 32-bit-truncated ms timestamp suitable for the +// MSG64.time field. The client compares these with GetTickCount(), which +// is also a 32-bit ms counter — exact origin doesn't matter, only that +// successive events have non-decreasing values within the wrap window. +func tickMillis() uint32 { + return uint32(time.Now().UnixMilli() & 0xFFFFFFFF) +} + func mustJSON(v any) []byte { b, err := json.Marshal(v) if err != nil { diff --git a/server/go/wsauth/wsauth.go b/server/go/wsauth/wsauth.go index 74beb8f..15b6b39 100644 --- a/server/go/wsauth/wsauth.go +++ b/server/go/wsauth/wsauth.go @@ -10,7 +10,11 @@ import ( "crypto/rand" "crypto/sha256" "encoding/hex" + "encoding/json" "errors" + "os" + "path/filepath" + "sort" "sync" "time" ) @@ -32,6 +36,12 @@ type User struct { PasswordHash string // SHA256(password+salt) in lowercase hex Salt string // empty for admin (matches C++ convention) Role string // "admin" or "viewer" + // AllowedGroups restricts which device groups this user can view. Empty + // slice = no device access (admin role is treated as "all" elsewhere + // without consulting this list). Matches the C++ WebUser.allowed_groups + // field one-for-one so users.json is interchangeable between the two + // servers. + AllowedGroups []string } // Session is the authenticated state attached to a valid token. @@ -48,6 +58,10 @@ type Authenticator struct { users map[string]*User // username -> user tokens map[string]*Session // token -> session tokenExpire time.Duration + // usersFile is the persistence target for non-admin accounts (admin + // lives in env/master-password only and is never written out, matching + // the C++ SaveUsers behavior). Empty disables persistence. + usersFile string } // New returns an empty Authenticator. Call AddUser to populate. @@ -92,6 +106,100 @@ func (a *Authenticator) AddAdminFromPlainPassword(username, plainPassword string }) } +// CreateUser registers a new account, persists the user table, and returns +// nil on success. Mirrors the C++ CWebService::CreateUser semantics: +// - role must be "admin" or "viewer" +// - "admin" is reserved for the bootstrap account and cannot be created +// via this API +// - duplicate usernames are rejected +// +// Password is hashed with a fresh per-user salt before being stored. +func (a *Authenticator) CreateUser(username, plainPassword, role string, allowedGroups []string) error { + if username == "" || plainPassword == "" { + return errors.New("username and password required") + } + if username == "admin" { + return errors.New("'admin' is reserved") + } + if role != "admin" && role != "viewer" { + return errors.New("role must be 'admin' or 'viewer'") + } + + salt, err := NewSalt() + if err != nil { + return err + } + u := User{ + Username: username, + PasswordHash: HashPassword(plainPassword, salt), + Salt: salt, + Role: role, + AllowedGroups: append([]string(nil), allowedGroups...), + } + + a.mu.Lock() + if _, exists := a.users[username]; exists { + a.mu.Unlock() + return errors.New("user already exists") + } + a.users[username] = &u + path := a.usersFile + snapshot := a.snapshotPersistableLocked() + a.mu.Unlock() + + if path != "" { + return writeUsersFile(path, snapshot) + } + return nil +} + +// DeleteUser removes a user account and persists the change. The bootstrap +// admin (always Role=="admin" with empty Salt) is protected: deleting it +// would lock everyone out of the UI with no way back in. +func (a *Authenticator) DeleteUser(username string) error { + if username == "" || username == "admin" { + return errors.New("cannot delete admin") + } + a.mu.Lock() + if _, ok := a.users[username]; !ok { + a.mu.Unlock() + return errors.New("user not found") + } + delete(a.users, username) + // Also drop any live sessions belonging to that user so they don't + // outlive their account. + for tok, s := range a.tokens { + if s.Username == username { + delete(a.tokens, tok) + } + } + path := a.usersFile + snapshot := a.snapshotPersistableLocked() + a.mu.Unlock() + + if path != "" { + return writeUsersFile(path, snapshot) + } + return nil +} + +// ListUsers returns a stable, copied snapshot of all users in name order. +// PasswordHash and Salt are zeroed in the returned records so callers can +// JSON-marshal the result without leaking credentials. +func (a *Authenticator) ListUsers() []User { + a.mu.RLock() + defer a.mu.RUnlock() + out := make([]User, 0, len(a.users)) + for _, u := range a.users { + c := *u + c.PasswordHash = "" + c.Salt = "" + out = append(out, c) + } + sort.Slice(out, func(i, j int) bool { return out[i].Username < out[j].Username }) + return out +} + // GetSalt returns the per-user salt. If the user does not exist, returns ("", false). // Note: the C++ admin uses an empty salt — that is still considered "found" // and the empty string is returned with ok=true. @@ -135,6 +243,134 @@ func (a *Authenticator) VerifyLogin(username, response, nonce string) (token, ro return token, u.Role, nil } +// usersFileEntry is the on-disk shape of one record in users.json. Field +// names match the C++ WebService SaveUsers output exactly so the two +// servers can share the file. +type usersFileEntry struct { + Username string `json:"username"` + PasswordHash string `json:"password_hash"` + Salt string `json:"salt"` + Role string `json:"role"` + AllowedGroups []string `json:"allowed_groups"` +} + +type usersFile struct { + Users []usersFileEntry `json:"users"` +} + +// SetUsersFile points the authenticator at a JSON file used to persist +// non-admin accounts and loads any existing entries. Calling it again with +// a different path is allowed but the previous file is not re-read on a +// nil-arg call — initialize once at startup. Missing files are not an +// error; they're treated as an empty user table. +func (a *Authenticator) SetUsersFile(path string) error { + a.mu.Lock() + a.usersFile = path + a.mu.Unlock() + if path == "" { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if len(data) == 0 { + return nil + } + var f usersFile + if err := json.Unmarshal(data, &f); err != nil { + return err + } + a.mu.Lock() + defer a.mu.Unlock() + for _, e := range f.Users { + // Skip admin: it's always sourced from env/master password and + // already injected via AddAdminFromPlainPassword. + if e.Username == "" || e.Username == "admin" { + continue + } + if e.PasswordHash == "" { + continue + } + role := e.Role + if role == "" { + role = "viewer" + } + a.users[e.Username] = &User{ + Username: e.Username, + PasswordHash: e.PasswordHash, + Salt: e.Salt, + Role: role, + AllowedGroups: append([]string(nil), e.AllowedGroups...), + } + } + return nil +} + +// snapshotPersistableLocked builds the on-disk slice while a.mu is held by +// the caller. Admin records are excluded — they live in env, not in the file. +func (a *Authenticator) snapshotPersistableLocked() []usersFileEntry { + out := make([]usersFileEntry, 0, len(a.users)) + for _, u := range a.users { + if u.Username == "admin" { + continue + } + groups := append([]string{}, u.AllowedGroups...) + out = append(out, usersFileEntry{ + Username: u.Username, + PasswordHash: u.PasswordHash, + Salt: u.Salt, + Role: u.Role, + AllowedGroups: groups, + }) + } + sort.Slice(out, func(i, j int) bool { return out[i].Username < out[j].Username }) + return out +} + +// writeUsersFile writes the user list atomically: encode to a temp file in +// the same directory, fsync, rename — so a crash mid-write can't leave the +// service starting up with a half-written users.json next time. +func writeUsersFile(path string, entries []usersFileEntry) error { + data, err := json.MarshalIndent(usersFile{Users: entries}, "", " ") + if err != nil { + return err + } + dir := filepath.Dir(path) + if dir != "" { + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + } + tmp, err := os.CreateTemp(dir, "users-*.json.tmp") + if err != nil { + return err + } + tmpName := tmp.Name() + if _, err := tmp.Write(data); err != nil { + _ = tmp.Close() + _ = os.Remove(tmpName) + return err + } + if err := tmp.Sync(); err != nil { + _ = tmp.Close() + _ = os.Remove(tmpName) + return err + } + if err := tmp.Close(); err != nil { + _ = os.Remove(tmpName) + return err + } + if err := os.Rename(tmpName, path); err != nil { + _ = os.Remove(tmpName) + return err + } + return nil +} + // ValidateToken returns the session for a token or ErrInvalidToken. Expired // tokens are removed lazily as they are looked up. func (a *Authenticator) ValidateToken(token string) (*Session, error) { -- 2.43.0 From 5947d41617b0a9ab18a5531e39c8017c68096ac2 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 18 May 2026 15:03:42 +0200 Subject: [PATCH 6/7] Feature(Go): Web terminal relay with PTY mode and graceful close (Phase 6) --- server/go/README.md | 133 +++++++++-------- server/go/cmd/main.go | 124 +++++++++++++++- server/go/hub/hub.go | 254 ++++++++++++++++++++++++++++++++- server/go/hub/hub_test.go | 6 + server/go/protocol/commands.go | 52 +++++-- server/go/web/ws.go | 73 +++++++++- server/go/web/ws_handlers.go | 150 +++++++++++++++++-- 7 files changed, 696 insertions(+), 96 deletions(-) diff --git a/server/go/README.md b/server/go/README.md index 27c6be1..dc94308 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -43,31 +43,59 @@ server/go/ ## 核心特性 +底层基础设施: + - **高并发**: 基于 Goroutine 池管理并发连接 -- **协议兼容**: 支持原有 C++ 客户端的多种协议标识 (Hell/Hello/Shine/Fuck) -- **协议头解密**: 支持8种协议头加密方式 (V0-V6 + Default) -- **授权验证**: 支持 TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权验证 -- **XOR编码**: 支持 XOREncoder16 数据编码/解码 -- **ZSTD 压缩**: 使用高效的 ZSTD 算法进行数据压缩 -- **GBK编码**: 自动将 Windows 客户端的 GBK 编码转换为 UTF-8 -- **线程安全**: Buffer、连接管理器和 LastActive 均为线程安全设计 -- **优雅关闭**: 支持信号处理和优雅停机,自动释放资源 -- **可配置**: 支持自定义端口、最大连接数、超时时间等 -- **日志系统**: 基于 zerolog,支持文件输出、日志轮转、客户端上下线记录 -- **Web UI 服务**: 内建 HTTP server,编译期 `//go:embed` 嵌入页面和静态资源,免外部文件依赖 +- **协议兼容**: 支持原有客户端的多种协议标识 (Hell/Hello/Shine/Fuck) +- **协议头解密**: 支持 8 种协议头加密方式 (V0-V6 + Default) +- **授权验证**: TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权 +- **XOR 编码 / ZSTD 压缩**: 与客户端完全兼容 +- **字符编码自适应**: 根据客户端能力位选择 UTF-8 直通或 GBK→UTF-8 转换 +- **线程安全 / 优雅关闭 / 多端口监听 / 结构化日志** + +Web 应用能力 (Phase 3-7): + +- **Web 鉴权**: challenge-response 登录 + 不透明 token,与 users.json schema 互通 +- **设备列表与监控**: 在线设备 / RTT / 活动窗口 / 分辨率 实时下发 +- **Web 远程桌面**: 浏览器 WebCodecs 解码 H.264,二进制 WS 帧低延迟中继;late-join 自动重发最近 IDR;优雅 BYE 关闭防止客户端无意义重连 +- **鼠标 / 键盘输入**: Win32 消息映射 (`WM_*` / `VK_*` / `MK_*`),MSG64 48 字节布局直传客户端 +- **Web 终端**: xterm.js + Windows ConPTY / 旧 cmd 管道双模式;二进制 "TRM1" 帧分流;尺寸自适应;单设备单 viewer +- **用户与分组**: admin 可创建/删除 viewer 账号、配置 allowed_groups,users.json 原子写入 ## 支持的命令 -当前已实现以下命令处理: +### 客户端 → 服务端 -| 命令 | 值 | 说明 | -|------|-----|------| -| TOKEN_AUTH | 100 | 授权请求 (验证 SN + Passcode + HMAC) | -| TOKEN_HEARTBEAT | 101 | 心跳包 (支持 HMAC 授权验证,返回 Authorized 状态) | -| TOKEN_LOGIN | 102 | 客户端登录 | -| CMD_HEARTBEAT_ACK | 216 | 心跳响应 (包含 Authorized 字段) | +| Token | 值 | 用途 | +| ---- | ---- | ---- | +| `TOKEN_AUTH` | 100 | 授权请求(SN + Passcode + HMAC) | +| `TOKEN_HEARTBEAT` | 101 | 心跳包(携带 ActiveWnd / Ping / SN) | +| `TOKEN_LOGIN` | 102 | 主连接登录 | +| `TOKEN_BITMAPINFO` | 115 | 屏幕子连接首包,含分辨率 + clientID | +| `TOKEN_FIRSTSCREEN` | 116 | 原始 BGRA 首帧(Go 侧丢弃) | +| `TOKEN_NEXTSCREEN` | 117 | H.264 屏幕帧 | +| `TOKEN_SHELL_START` | 128 | 旧 cmd-pipe 终端子连接首包 | +| `TOKEN_KEYFRAME` | 134 | GOP 关键帧(DEFAULT_GOP 无限大,实际未用) | +| `TOKEN_TERMINAL_START` | 232 | PTY 终端子连接首包 | +| `TOKEN_TERMINAL_CLOSE` | 233 | 终端关闭通知 | +| `TOKEN_CONN_AUTH` | 246 | 子连接身份握手,含 clientID | +| (raw bytes) | — | 终端 sub-conn 绑定后裸字节即 shell 输出 | -其他命令会被记录为 Debug 日志,可按需扩展。 +### 服务端 → 客户端 + +| Command | 值 | 用途 | +| ---- | ---- | ---- | +| `COMMAND_SCREEN_SPY` | 16 | 启动屏幕捕获 | +| `COMMAND_SCREEN_CONTROL` | 20 | 鼠标 / 键盘输入(MSG64 批次) | +| `COMMAND_NEXT` | 30 | 解除客户端读线程阻塞 | +| `COMMAND_SHELL` | 40 | 请求开启 shell 子连接 | +| `CMD_TERMINAL_RESIZE` | 81 | PTY 尺寸 (cols / rows int16 LE) | +| `COMMAND_BYE` | 204 | 优雅断开屏幕 / 终端 | +| `CMD_MASTERSETTING` | 215 | 主控配置 + HMAC 签名 (1000B) | +| `CMD_HEARTBEAT_ACK` | 216 | 心跳响应(携带 Authorized 字段) | +| `TOKEN_CONN_AUTH` | 246 | 子连接身份握手响应 (256B) | + +未列出的命令字节会被记录为 Debug 日志,按需扩展。 ## 快速开始 @@ -136,6 +164,10 @@ $env:YAMA_PWD="your_super_password" ## 使用示例 +完整的 TCP + Hub + Web 集成示例就是 [`cmd/main.go`](cmd/main.go),那是程序入口本身、也是最权威的范例 —— 包含 handler 装配、hub 注册、web HTTP/WS 服务、信号优雅关闭等。 + +如果只想用 TCP 框架做自定义服务端(不要 Web/Hub),最小示例如下: + ```go package main @@ -150,57 +182,32 @@ import ( "github.com/yuanyuanxiang/SimpleRemoter/server/go/server" ) -// 实现 Handler 接口 -type MyHandler struct { - log *logger.Logger -} - -func (h *MyHandler) OnConnect(ctx *connection.Context) { - h.log.ClientEvent("online", ctx.ID, ctx.GetPeerIP()) -} - -func (h *MyHandler) OnDisconnect(ctx *connection.Context) { - h.log.ClientEvent("offline", ctx.ID, ctx.GetPeerIP()) -} +type MyHandler struct{ log *logger.Logger } +func (h *MyHandler) OnConnect(ctx *connection.Context) {} +func (h *MyHandler) OnDisconnect(ctx *connection.Context) {} func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) { if len(data) == 0 { return } - cmd := data[0] - switch cmd { - case protocol.TokenLogin: + if data[0] == protocol.TokenLogin { info, _ := protocol.ParseLoginInfo(data) - h.log.Info("Client login: %s (%s)", info.PCName, info.OsVerInfo) - case protocol.TokenHeartbeat: - h.log.Debug("Heartbeat from client %d", ctx.ID) + h.log.Info("login: %s (%s)", info.PCName, info.OsVerInfo) } } func main() { - // 配置日志 (控制台 + 文件) - logCfg := logger.DefaultConfig() - logCfg.File = "logs/server.log" - log := logger.New(logCfg) - - // 配置服务器 - config := server.DefaultConfig() - config.Port = 6543 - - // 创建并启动服务器 - srv := server.New(config) + log := logger.New(logger.DefaultConfig()) + srv := server.New(server.DefaultConfig()) srv.SetLogger(log.WithPrefix("Server")) srv.SetHandler(&MyHandler{log: log}) - if err := srv.Start(); err != nil { - log.Fatal("启动失败: %v", err) + log.Fatal("start: %v", err) } - // 等待退出信号 - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig srv.Stop() } ``` @@ -286,7 +293,7 @@ func main() { | bWebCamExist | 448 | 4 | 是否有摄像头 | | dwSpeed | 452 | 4 | 网速 | | szStartTime | 456 | 20 | 启动时间 | -| szReserved | 476 | 512 | 扩展字段 (用`|`分隔) | +| szReserved | 476 | 512 | 扩展字段(多字段以 `\|` 分隔) | ### Heartbeat 结构 @@ -410,15 +417,19 @@ publicIP := info.GetReservedField(11) // 公网 IP ## 与 C++ 版本对比 | 特性 | C++ (IOCP) | Go | -|------|------------|-----| +| ---- | ---- | ---- | | 并发模型 | IOCP + 线程池 | Goroutine 池 | -| 压缩算法 | ZSTD | ZSTD | | 跨平台 | Windows | 全平台 | | 内存管理 | 手动 | GC | | 代码复杂度 | 高 | 低 | -| 协议头解密 | 8种方式 | 8种方式 | -| XOR编码 | XOREncoder16 | XOREncoder16 | -| 字符编码 | GBK | GBK -> UTF-8 | +| 压缩 / XOR / 头加密 | 完整 8 套加密方式 + XOREncoder16 + ZSTD | 完全对齐 | +| 字符编码 | GBK | UTF-8 直通 / GBK→UTF-8 (按客户端能力位) | +| 设备列表与监控 | MFC 列表控件 | Web UI | +| Web 远程桌面 | 内嵌浏览器 + H.264 | 完全对齐(WebCodecs 解码) | +| 鼠标键盘转发 | 已实现 | 完全对齐 | +| Web 终端 | 内嵌 xterm.js + ConPTY | 完全对齐(含旧 cmd-pipe 兼容) | +| 用户 / 分组管理 | 已实现 | users.json schema 互通 | +| 文件传输 / 摄像头 / 录音 等 | 已实现 | 暂未实现(按需扩展) | ## 依赖 diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index cbda53d..56d602c 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -37,17 +37,33 @@ func (h *MyHandler) OnConnect(ctx *connection.Context) { // OnDisconnect is called when a client disconnects func (h *MyHandler) OnDisconnect(ctx *connection.Context) { - // Always clean up any screen sub-context mapping first — the connection - // may be a screen sub-conn (which has no ClientInfo) rather than a main - // login connection. UnbindScreenConn is a no-op if not tracked. + // Always clean up any sub-context mapping first — the connection may + // be a screen / terminal sub-conn rather than a main login connection. + // Both Unbind* calls are no-ops if not tracked. UnbindTerminalConn + // also fires OnTerminalClosed so the browser sees the session end on + // unexpected device-side drops. h.hub.UnbindScreenConn(ctx) + h.hub.UnbindTerminalConn(ctx) info := ctx.GetInfo() - if info.ClientID != "" { + // Only treat this disconnect as a device-going-offline event if this + // ctx is the device's MAIN login connection. Phase 6 added ClientID + // pinning to sub-conns (via ConnAuth — needed for terminal routing), + // so a non-empty ClientID alone no longer distinguishes main from + // sub. Closing a screen / terminal sub-conn must NOT remove the + // device from the hub. + if info.ClientID != "" && h.hub.MainConn(info.ClientID) == ctx { h.log.ClientEvent("offline", ctx.ID, ctx.GetPeerIP(), "clientID", info.ClientID, "computer", info.ComputerName, ) + // Tear down any active sub-conn sessions BEFORE Unregister so the + // browser sees screen/terminal close events alongside the + // device-offline event, instead of frames/output continuing to + // stream from orphaned sub-conn ctxs until they time out on + // their own. Both calls no-op if there's no active session. + h.hub.CloseScreen(info.ClientID) + h.hub.CloseTerminalSession(info.ClientID) h.hub.Unregister(info.ClientID) } } @@ -58,6 +74,27 @@ func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) { return } + // Terminal-bound sub-conns deliver RAW shell output with no leading + // command byte — see client/ConPTYManager.cpp:328 (Send2Server with + // just the buffer). We must short-circuit BEFORE the command switch + // or the first output byte will be misinterpreted as a token. + // Exception: a length-1 packet whose byte is TOKEN_TERMINAL_CLOSE + // is the device's "shell exited" notification, NOT data. + if devID := h.hub.TerminalDeviceID(ctx); devID != "" { + if len(data) == 1 && data[0] == protocol.TokenTerminalClose { + h.log.Info("terminal closed by device=%s conn=%d", devID, ctx.ID) + h.hub.CloseTerminalSession(devID) + return + } + // Wrap with the 'TRM1' magic the browser uses to demultiplex + // terminal output from screen frames over the shared WS. + packet := make([]byte, 4+len(data)) + copy(packet[:4], protocol.TerminalBinaryMagic[:]) + copy(packet[4:], data) + h.hub.PublishTerminalData(devID, packet) + return + } + cmd := data[0] // Handle commands switch cmd { @@ -71,6 +108,17 @@ func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) { h.handleConnAuth(ctx, data) case protocol.TokenBitmapInfo: h.handleBitmapInfo(ctx, data) + case protocol.TokenTerminalStart: + h.handleTerminalStart(ctx, true) + case protocol.TokenShellStart: + h.handleTerminalStart(ctx, false) + case protocol.TokenTerminalClose: + // Pre-bind close (rare — device gives up before the server + // finished its half of the handshake). Best-effort cleanup. + if devID := h.deviceIDOfSubConn(ctx); devID != "" { + h.log.Info("pre-bind terminal close: device=%s conn=%d", devID, ctx.ID) + h.hub.CloseTerminalSession(devID) + } case protocol.TokenFirstScreen: // TOKEN_FIRSTSCREEN delivers a RAW BGRA baseline frame, not an // H264 unit — bytes ≈ width × height × 4. The C++ MFC dialog @@ -107,7 +155,24 @@ func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) { // and the signing primitive lives in a vendored component out of scope // for this server, so we always reply OK and let TOKEN_BITMAPINFO carry // the device ID via offset 41 when the screen sub-conn proceeds. -func (h *MyHandler) handleConnAuth(ctx *connection.Context, _ []byte) { +func (h *MyHandler) handleConnAuth(ctx *connection.Context, data []byte) { + // Pin the parent device's ClientID onto the sub-conn. Without this, + // later 1-byte tokens (TOKEN_TERMINAL_START / TOKEN_SHELL_START) have + // no way to identify which device they belong to — they carry no + // clientID themselves. ConnAuthPacket layout has clientID at offset 1 + // (uint64 LE); see common/commands.h::ConnAuthPacket. + if len(data) >= protocol.ConnAuthOffClientID+8 { + clientID := binary.LittleEndian.Uint64( + data[protocol.ConnAuthOffClientID : protocol.ConnAuthOffClientID+8]) + if clientID != 0 { + // Sub-conns never go through handleLogin, so their ctx.Info + // is otherwise empty. We only need ClientID for routing. + info := ctx.GetInfo() + info.ClientID = strconv.FormatUint(clientID, 10) + ctx.SetInfo(info) + } + } + ack := make([]byte, protocol.ConnAuthAckSize) ack[0] = protocol.TokenConnAuth ack[protocol.ConnAuthAckOffStatus] = protocol.ConnAuthStatusOK @@ -119,6 +184,55 @@ func (h *MyHandler) handleConnAuth(ctx *connection.Context, _ []byte) { } } +// deviceIDOfSubConn resolves the parent device of a sub-conn from the +// ClientID pinned by handleConnAuth. Returns "" for the rare case of a +// legacy client that skipped ConnAuth (the Go server's only target is +// modern clients, so this is effectively a paranoia check). +func (h *MyHandler) deviceIDOfSubConn(ctx *connection.Context) string { + return ctx.GetInfo().ClientID +} + +// handleTerminalStart fires when the device's freshly-spawned shell +// sub-conn announces itself. TOKEN_TERMINAL_START (232) means PTY mode +// (Linux/macOS or Windows ConPTY); TOKEN_SHELL_START (128) means the +// legacy Windows cmd-pipe path. Both packets are 1-byte tokens — the +// device identity comes from ConnAuth's pinned ClientID. +// +// After binding we send: +// - For PTY only: an initial CMD_TERMINAL_RESIZE 80x24 so the shell +// doesn't render at the PTY default before the browser's first fit. +// vim/htop look broken otherwise. The browser will follow up with a +// real term_resize once xterm.js sizes the canvas. +// - Always: COMMAND_NEXT to unblock the device's read thread (the +// ConPTYManager ReadThread sits on m_hEventDlgOpen until then — +// see client/ConPTYManager.cpp:259). +func (h *MyHandler) handleTerminalStart(ctx *connection.Context, isPTY bool) { + devID := h.deviceIDOfSubConn(ctx) + if devID == "" { + h.log.Warn("terminal start with no clientID: conn=%d", ctx.ID) + ctx.Close() + return + } + if !h.hub.BindTerminalConn(devID, ctx, isPTY) { + // No pending session — this is a stale sub-conn (e.g. browser + // gave up and closed term_close already). Drop it. + h.log.Warn("orphan terminal sub-conn: device=%s conn=%d isPTY=%v", + devID, ctx.ID, isPTY) + ctx.Close() + return + } + + if isPTY { + if err := h.srv.Send(ctx, protocol.BuildTerminalResize(80, 24)); err != nil { + h.log.Error("initial resize send failed: conn=%d: %v", ctx.ID, err) + } + } + if err := h.srv.Send(ctx, []byte{protocol.CommandNext}); err != nil { + h.log.Error("COMMAND_NEXT send failed on terminal: conn=%d: %v", ctx.ID, err) + } + h.log.Info("terminal bound: device=%s conn=%d isPTY=%v", devID, ctx.ID, isPTY) +} + // handleBitmapInfo is the first packet on a freshly-arrived screen // sub-connection. Packet layout (after the command byte at data[0]): // diff --git a/server/go/hub/hub.go b/server/go/hub/hub.go index f600fd6..9c25e7d 100644 --- a/server/go/hub/hub.go +++ b/server/go/hub/hub.go @@ -77,6 +77,21 @@ type Device struct { // JSON messages. cursorSeen bool lastCursorIndex byte + + // Terminal session state — at most one web terminal per device (MVP + // constraint shared with the C++ server). All three fields are + // guarded by hub.mu. + // + // terminalPending: COMMAND_SHELL has been sent, waiting for the device's + // sub-conn to arrive and announce itself via TOKEN_TERMINAL_START / + // TOKEN_SHELL_START. + // terminalConn: the shell sub-conn ctx after binding. Nil before BIND + // and after teardown. + // terminalIsPTY: distinguishes Linux/macOS/ConPTY (true) from the legacy + // Windows cmd-pipe path. PTY mode supports resize; cmd-pipe ignores it. + terminalPending bool + terminalConn *connection.Context + terminalIsPTY bool } // ScreenCache is a read-only snapshot of a device's last-seen screen state, @@ -171,6 +186,18 @@ type EventHandler interface { // Duplicates (same index as the previous frame) are filtered out by the // hub before reaching subscribers. OnCursorChange(deviceID string, index byte) + // OnTerminalReady fires once the device's shell sub-conn is bound and + // the server has sent COMMAND_NEXT to start its output read loop. + // isPTY=true means PTY mode (Linux/macOS or ConPTY); false means the + // legacy Windows cmd-pipe path which doesn't support resize. + OnTerminalReady(deviceID string, isPTY bool) + // OnTerminalData ships one chunk of raw shell output (already wrapped + // in the WS-binary "TRM1" magic header) to terminal viewers. + OnTerminalData(deviceID string, packet []byte) + // OnTerminalClosed fires when the session ends — either because the + // device sent TOKEN_TERMINAL_CLOSE, the sub-conn dropped, or the + // server explicitly tore it down. + OnTerminalClosed(deviceID string, reason string) } // Hub is a thread-safe registry of online devices. @@ -187,13 +214,19 @@ type Hub struct { // having to walk every device. Empty when no screen sessions exist. screenIndex map[*connection.Context]string screenIndexMu sync.RWMutex + + // Parallel reverse index for terminal sub-conns. Same purpose: O(1) + // lookup from a raw ctx (e.g. on OnDisconnect) back to its device. + terminalIndex map[*connection.Context]string + terminalIndexMu sync.RWMutex } // New returns an empty Hub. func New() *Hub { return &Hub{ - devices: make(map[string]*Device), - screenIndex: make(map[*connection.Context]string), + devices: make(map[string]*Device), + screenIndex: make(map[*connection.Context]string), + terminalIndex: make(map[*connection.Context]string), } } @@ -547,6 +580,223 @@ func (h *Hub) UpdateLive(id string, rtt int, activeWindow string) { } } +// ----- Terminal session management (Phase 6) -------------------------------- + +// ErrTerminalBusy is returned by OpenTerminalSession when the device already +// has a pending or active terminal session — MVP enforces single-viewer. +var ErrTerminalBusy = errors.New("terminal already open by another viewer") + +// OpenTerminalSession atomically marks a terminal session as pending for the +// device, then sends COMMAND_SHELL on the main TCP connection so the device +// will spawn a shell sub-conn. Returns nil if the request was sent. On any +// failure the pending flag is rolled back so retries are possible. +// +// Single-viewer constraint: if a pending or bound session already exists, +// returns ErrTerminalBusy. Mirrors C++ CWebService::HandleTermOpen +// (server/2015Remote/WebService.cpp:1838). +func (h *Hub) OpenTerminalSession(deviceID string) error { + if deviceID == "" { + return ErrDeviceOffline + } + h.mu.Lock() + d, ok := h.devices[deviceID] + if !ok || d.conn == nil { + h.mu.Unlock() + return ErrDeviceOffline + } + if d.terminalPending || d.terminalConn != nil { + h.mu.Unlock() + return ErrTerminalBusy + } + d.terminalPending = true + mainConn := d.conn + h.mu.Unlock() + + if h.sender == nil { + // Roll back so a retry isn't permanently blocked. + h.mu.Lock() + d.terminalPending = false + h.mu.Unlock() + return ErrNoSender + } + if err := h.sender(mainConn, []byte{protocol.CommandShell}); err != nil { + h.mu.Lock() + d.terminalPending = false + h.mu.Unlock() + return err + } + return nil +} + +// IsTerminalPending tells the TCP layer whether the next-arriving shell +// sub-conn should be claimed by the web terminal. The C++ side uses this +// in MessageHandle to decide between WebService takeover and opening an +// MFC dialog (server/2015Remote/2015RemoteDlg.cpp:5753). +func (h *Hub) IsTerminalPending(deviceID string) bool { + h.mu.RLock() + defer h.mu.RUnlock() + d, ok := h.devices[deviceID] + return ok && d.terminalPending +} + +// BindTerminalConn promotes the pending session to an active one by +// associating the device's freshly-arrived shell sub-conn. Returns false +// if no pending session exists — callers should drop the orphan ctx. +// +// Subscribers receive OnTerminalReady AFTER binding so they can flip the +// browser into "ready" state immediately on the same TCP roundtrip that +// will deliver the first shell output. +func (h *Hub) BindTerminalConn(deviceID string, ctx *connection.Context, isPTY bool) bool { + if deviceID == "" || ctx == nil { + return false + } + h.mu.Lock() + d, ok := h.devices[deviceID] + if !ok || !d.terminalPending { + h.mu.Unlock() + return false + } + d.terminalConn = ctx + d.terminalIsPTY = isPTY + d.terminalPending = false + h.mu.Unlock() + + h.terminalIndexMu.Lock() + h.terminalIndex[ctx] = deviceID + h.terminalIndexMu.Unlock() + + for _, s := range h.snapshotSubscribers() { + s.OnTerminalReady(deviceID, isPTY) + } + return true +} + +// TerminalDeviceID returns the device ID whose terminal sub-conn this +// context belongs to, or "" otherwise. The TCP layer uses this on every +// inbound packet on a sub-conn — when non-empty, the bytes are raw shell +// output and bypass the usual command-byte switch. +func (h *Hub) TerminalDeviceID(ctx *connection.Context) string { + h.terminalIndexMu.RLock() + defer h.terminalIndexMu.RUnlock() + return h.terminalIndex[ctx] +} + +// UnbindTerminalConn removes the terminal mapping (called from the TCP +// disconnect path for any sub-conn ctx). Fires OnTerminalClosed once if +// the unbind actually removed something — so subscribers can update the +// browser even on unexpected device-side drops. +func (h *Hub) UnbindTerminalConn(ctx *connection.Context) { + h.terminalIndexMu.Lock() + deviceID, tracked := h.terminalIndex[ctx] + if !tracked { + h.terminalIndexMu.Unlock() + return + } + delete(h.terminalIndex, ctx) + h.terminalIndexMu.Unlock() + + h.mu.Lock() + if d, ok := h.devices[deviceID]; ok && d.terminalConn == ctx { + d.terminalConn = nil + d.terminalPending = false + d.terminalIsPTY = false + } + h.mu.Unlock() + + for _, s := range h.snapshotSubscribers() { + s.OnTerminalClosed(deviceID, "disconnected") + } +} + +// SendToTerminal forwards bytes (typically xterm.js keystrokes) to the +// device's shell sub-conn. Returns ErrDeviceOffline if no session is +// active for this device. +func (h *Hub) SendToTerminal(id string, data []byte) error { + h.mu.RLock() + d, ok := h.devices[id] + var tc *connection.Context + if ok { + tc = d.terminalConn + } + h.mu.RUnlock() + if !ok || tc == nil { + return ErrDeviceOffline + } + if h.sender == nil { + return ErrNoSender + } + return h.sender(tc, data) +} + +// TerminalIsPTY reports whether the active session is PTY mode (the +// resize command only applies in PTY mode — legacy cmd-pipe ignores it). +func (h *Hub) TerminalIsPTY(id string) bool { + h.mu.RLock() + defer h.mu.RUnlock() + d, ok := h.devices[id] + return ok && d.terminalConn != nil && d.terminalIsPTY +} + +// CloseTerminalSession tears down the session from the server side +// (typically when the requesting browser sends term_close or disconnects). +// Mirrors CloseScreen's graceful pattern: drop the index synchronously, +// send COMMAND_BYE, then close after a short grace period so the client's +// IOCPClient reconnect logic doesn't fire. +func (h *Hub) CloseTerminalSession(deviceID string) { + h.mu.Lock() + d, ok := h.devices[deviceID] + if !ok { + h.mu.Unlock() + return + } + tc := d.terminalConn + // hadSession guards against firing spurious OnTerminalClosed events + // when there was nothing to tear down — relevant when the main-conn + // teardown path calls CloseTerminalSession unconditionally as part of + // device-offline cleanup, or when both OnDisconnect and an explicit + // browser term_close race for the same teardown. + hadSession := tc != nil || d.terminalPending + d.terminalConn = nil + d.terminalPending = false + d.terminalIsPTY = false + h.mu.Unlock() + + if !hadSession { + return + } + + for _, s := range h.snapshotSubscribers() { + s.OnTerminalClosed(deviceID, "closed") + } + + if tc == nil { + return + } + h.terminalIndexMu.Lock() + delete(h.terminalIndex, tc) + h.terminalIndexMu.Unlock() + + // Mirror Hub.CloseScreen: send COMMAND_BYE then close after 500 ms so + // the device exits its shell read loop instead of treating the FIN as + // a network blip and triggering reconnect. + if h.sender != nil { + _ = h.sender(tc, []byte{protocol.CommandBye}) + } + go func(c *connection.Context) { + time.Sleep(500 * time.Millisecond) + c.Close() + }(tc) +} + +// PublishTerminalData fans out one chunk of shell output to subscribers. +// Caller has already wrapped it in the "TRM1" magic header so the browser +// can demultiplex from screen frames over the shared WebSocket. +func (h *Hub) PublishTerminalData(deviceID string, packet []byte) { + for _, s := range h.snapshotSubscribers() { + s.OnTerminalData(deviceID, packet) + } +} + // Count returns the current number of online devices. func (h *Hub) Count() int { h.mu.RLock() diff --git a/server/go/hub/hub_test.go b/server/go/hub/hub_test.go index cbd8185..8df9c8d 100644 --- a/server/go/hub/hub_test.go +++ b/server/go/hub/hub_test.go @@ -84,6 +84,12 @@ func (c *captureHandler) OnResolutionChange(_ string, _, _ int) {} func (c *captureHandler) OnCursorChange(_ string, _ byte) {} +func (c *captureHandler) OnTerminalReady(_ string, _ bool) {} + +func (c *captureHandler) OnTerminalData(_ string, _ []byte) {} + +func (c *captureHandler) OnTerminalClosed(_ string, _ string) {} + func TestHubSubscribeEvents(t *testing.T) { h := New() c := &captureHandler{} diff --git a/server/go/protocol/commands.go b/server/go/protocol/commands.go index 09fe249..0ae95d9 100644 --- a/server/go/protocol/commands.go +++ b/server/go/protocol/commands.go @@ -107,19 +107,24 @@ const ( CommandScreenSpy byte = 16 // COMMAND_SCREEN_SPY - start screen capture CommandScreenControl byte = 20 // COMMAND_SCREEN_CONTROL - mouse/keyboard input (MSG64 batches) CommandNext byte = 30 // COMMAND_NEXT - "control-side dialog is open, you may stream" + CommandShell byte = 40 // COMMAND_SHELL - ask device to open a shell sub-connection + CommandTerminalRsize byte = 81 // CMD_TERMINAL_RESIZE - [cmd:1][cols:2 LE][rows:2 LE] CommandBye byte = 204 // COMMAND_BYE - disconnect CommandHeartbeat byte = 216 // CMD_HEARTBEAT_ACK // Client -> Server tokens - TokenAuth byte = 100 // TOKEN_AUTH - authorization required - TokenHeartbeat byte = 101 // TOKEN_HEARTBEAT - TokenLogin byte = 102 // TOKEN_LOGIN - login packet - TokenBitmapInfo byte = 115 // TOKEN_BITMAPINFO - screen sub-connection header - TokenFirstScreen byte = 116 // TOKEN_FIRSTSCREEN - raw BGRA baseline frame (NOT H264) - TokenNextScreen byte = 117 // TOKEN_NEXTSCREEN - non-keyframe H264 (P-frame) - TokenKeyframe byte = 134 // TOKEN_KEYFRAME - H264 IDR (sent on GOP boundary) - TokenConnAuth byte = 246 // TOKEN_CONN_AUTH - sub-connection identity handshake - CmdCursorImage byte = 93 // CMD_CURSOR_IMAGE - custom cursor bitmap (Phase 5+ feature) + TokenAuth byte = 100 // TOKEN_AUTH - authorization required + TokenHeartbeat byte = 101 // TOKEN_HEARTBEAT + TokenLogin byte = 102 // TOKEN_LOGIN - login packet + TokenBitmapInfo byte = 115 // TOKEN_BITMAPINFO - screen sub-connection header + TokenFirstScreen byte = 116 // TOKEN_FIRSTSCREEN - raw BGRA baseline frame (NOT H264) + TokenNextScreen byte = 117 // TOKEN_NEXTSCREEN - non-keyframe H264 (P-frame) + TokenShellStart byte = 128 // TOKEN_SHELL_START - legacy cmd-pipe shell sub-conn open + TokenKeyframe byte = 134 // TOKEN_KEYFRAME - H264 IDR (sent on GOP boundary) + TokenTerminalStart byte = 232 // TOKEN_TERMINAL_START - modern PTY shell sub-conn open + TokenTerminalClose byte = 233 // TOKEN_TERMINAL_CLOSE - shell exited / close ack + TokenConnAuth byte = 246 // TOKEN_CONN_AUTH - sub-connection identity handshake + CmdCursorImage byte = 93 // CMD_CURSOR_IMAGE - custom cursor bitmap (Phase 5+ feature) ) // Sub-connection authentication (matches common/commands.h ConnAuth* structs). @@ -128,7 +133,13 @@ const ( const ( ConnAuthPacketSize = 512 ConnAuthAckSize = 256 - // ConnAuthAck field offsets within the 256-byte buffer. + // ConnAuthPacket field offsets within the inbound 512-byte buffer. + // Layout (from common/commands.h::ConnAuthPacket): + // [token:1][clientID:8 LE][timestamp:8 LE][nonce:16][signature:64][reserved:415] + ConnAuthOffClientID = 1 // uint64 LE — pin to the sub-conn so later + // // 1-byte tokens (TOKEN_TERMINAL_START etc.) can + // // resolve the parent device. + // ConnAuthAck field offsets within the outbound 256-byte buffer. ConnAuthAckOffStatus = 1 // uint8 ConnAuthAckOffServerTime = 2 // uint64 LE // Status codes. @@ -245,6 +256,27 @@ func BuildScreenControlPacket(message, wParam, lParam uint64, ptX, ptY int32, ti return buf } +// TerminalBinaryMagic is the 4-byte prefix the web UI uses to demultiplex +// terminal output from screen frames over the single WebSocket. Matches +// the C++ side at server/2015Remote/WebService.cpp:2013 ("TRM1"). Screen +// frames lead with a uint32 LE device ID, so collisions with this exact +// magic are astronomically rare in practice. +var TerminalBinaryMagic = [4]byte{'T', 'R', 'M', '1'} + +// BuildTerminalResize encodes the 5-byte CMD_TERMINAL_RESIZE packet the +// client's ConPTYManager/TerminalManager expects on the shell sub-conn: +// +// [CMD_TERMINAL_RESIZE:1][cols:2 LE][rows:2 LE] +// +// cols/rows are signed int16 on the wire (the C++ side casts to `short`). +func BuildTerminalResize(cols, rows int) []byte { + buf := make([]byte, 5) + buf[0] = CommandTerminalRsize + binary.LittleEndian.PutUint16(buf[1:3], uint16(int16(cols))) + binary.LittleEndian.PutUint16(buf[3:5], uint16(int16(rows))) + return buf +} + // MakeLParam packs x into the low word and y into the high word — the // Windows MAKELPARAM macro the client expects in mouse-message lParams. func MakeLParam(x, y int32) uint64 { diff --git a/server/go/web/ws.go b/server/go/web/ws.go index 13d07cb..5b7b9c5 100644 --- a/server/go/web/ws.go +++ b/server/go/web/ws.go @@ -46,11 +46,12 @@ type wsClient struct { once sync.Once // Mutated under wsHub.mu (or only by the read loop owning this client). - nonce string // outstanding challenge — cleared after a successful login - token string // set once authenticated - role string // mirrors session role after login - addr string // client address for logs - watching string // device ID this browser is currently streaming, "" when on the list + nonce string // outstanding challenge — cleared after a successful login + token string // set once authenticated + role string // mirrors session role after login + addr string // client address for logs + watching string // device ID this browser is currently streaming, "" when on the list + termWatching string // device ID for an open web terminal session, "" otherwise } // queue writes a JSON text frame onto the send buffer. Drops silently if the @@ -176,6 +177,61 @@ func (h *wsHub) OnScreenFrame(deviceID string, packet []byte, _ bool) { } } +// OnTerminalReady notifies the requesting browser that its term_open +// handshake completed. mode is "pty" or "legacy" — xterm.js disables the +// resize callback in legacy mode (no PTY behind the cmd pipe). +func (h *wsHub) OnTerminalReady(deviceID string, isPTY bool) { + mode := "legacy" + if isPTY { + mode = "pty" + } + msg := mustJSON(map[string]any{ + "cmd": "term_ready", + "id": deviceID, + "mode": mode, + }) + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.termWatching == deviceID && c.token != "" { + c.queue(msg) + } + } +} + +// OnTerminalData ships one chunk of raw shell output (already wrapped in +// the "TRM1" magic header) over the binary WS frame. Single-viewer is +// enforced upstream so at most one client matches per device. +func (h *wsHub) OnTerminalData(deviceID string, packet []byte) { + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + if c.termWatching == deviceID && c.token != "" { + c.queueBinary(packet) + } + } +} + +// OnTerminalClosed fires when the device's shell exits or the sub-conn +// drops. The browser closes its xterm panel. We also clear termWatching +// so a subsequent term_open from the same browser isn't rejected as +// "already open" by stale state. +func (h *wsHub) OnTerminalClosed(deviceID string, reason string) { + msg := mustJSON(map[string]any{ + "cmd": "term_closed", + "ok": true, + "reason": reason, + }) + h.mu.Lock() + defer h.mu.Unlock() + for c := range h.clients { + if c.termWatching == deviceID && c.token != "" { + c.termWatching = "" + c.queue(msg) + } + } +} + // OnDeviceUpdate forwards heartbeat-derived liveness data so the device-list // rows can refresh RTT and active-window labels without re-fetching. func (h *wsHub) OnDeviceUpdate(id string, rtt int, activeWindow string) { @@ -221,6 +277,13 @@ func (h *wsHub) unregister(c *wsClient) { if c.watching != "" && h.countWatchers(c.watching) == 0 { h.devices.CloseScreen(c.watching) } + // Terminal sessions are single-viewer by design, so any open session + // belongs to this client. Tear it down so the next viewer doesn't + // hit ErrTerminalBusy from an abandoned session. + if c.termWatching != "" { + h.devices.CloseTerminalSession(c.termWatching) + c.termWatching = "" + } // Do NOT revoke the token: tokens are session-scoped, not WS-scoped. // Frontend may close+reopen the WS at any time (visibilitychange handler, // brief network blip, reload) and must be able to resume with the same diff --git a/server/go/web/ws_handlers.go b/server/go/web/ws_handlers.go index cb5906f..f1202fa 100644 --- a/server/go/web/ws_handlers.go +++ b/server/go/web/ws_handlers.go @@ -15,8 +15,8 @@ import ( // Phase 3 implements: get_salt, login, get_devices, ping, disconnect. // Phase 4 adds: connect, screen frame relay. // Phase 5 adds: mouse, key (input forwarding to the device screen sub-conn). -// Phase 6/7 commands (term_*, user mgmt) get a friendly "not yet implemented" -// reply so the browser UI doesn't hang silently. +// Phase 6 adds: term_open / term_input / term_resize / term_close (PTY relay). +// Phase 7 covers admin: create_user / delete_user / list_users / get_groups. func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { switch cmd { case "get_salt": @@ -39,9 +39,13 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { case "key": h.handleKey(c, raw) case "term_open": - h.replyNotImplemented(c, "term_closed", "Web terminal not yet implemented on Go server") - case "term_input", "term_resize", "term_close": - // silently ignored — no terminal session + h.handleTermOpen(c, raw) + case "term_input": + h.handleTermInput(c, raw) + case "term_resize": + h.handleTermResize(c, raw) + case "term_close": + h.handleTermClose(c, raw) // Admin operations (Phase 7). case "create_user": @@ -55,14 +59,6 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { } } -func (h *wsHub) replyNotImplemented(c *wsClient, replyCmd, msg string) { - c.queue(mustJSON(map[string]any{ - "cmd": replyCmd, - "ok": false, - "msg": msg, - })) -} - // requireAdmin combines token validation with a role=="admin" check. The // reply on failure has the standard `{cmd, ok:false, msg}` shape so the // front-end's generic toast handler can surface the reason. @@ -555,6 +551,134 @@ func (h *wsHub) handleGetGroups(c *wsClient, raw []byte) { })) } +// handleTermOpen kicks off a web terminal session. On success the wsClient +// records `termWatching = deviceID` so subsequent term_input / term_resize +// have a target, and the hub sends COMMAND_SHELL to the device. The +// device's shell sub-conn arrives separately and is bound by the TCP layer +// via Hub.BindTerminalConn; that step fires OnTerminalReady to flip the +// browser into "ready" state. +// +// Single-viewer is enforced at the hub. The C++ side matches: +// server/2015Remote/WebService.cpp:1799. +func (h *wsHub) handleTermOpen(c *wsClient, raw []byte) { + const replyCmd = "term_closed" + if !h.requireAuth(c, raw, replyCmd) { + return + } + var in struct { + ID string `json:"id"` + } + if err := json.Unmarshal(raw, &in); err != nil || in.ID == "" { + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false, "msg": "Bad request"})) + return + } + + // Pin termWatching BEFORE asking the hub to open the session: the + // device's shell sub-conn can arrive in <100 ms on LAN, and + // OnTerminalReady filters by termWatching. Same race shape as the + // screen path in handleConnect. + h.mu.Lock() + if c.termWatching != "" && c.termWatching != in.ID { + h.mu.Unlock() + c.queue(mustJSON(map[string]any{ + "cmd": replyCmd, "ok": false, + "msg": "Close current terminal before opening another", + })) + return + } + c.termWatching = in.ID + h.mu.Unlock() + + if err := h.devices.OpenTerminalSession(in.ID); err != nil { + h.mu.Lock() + c.termWatching = "" + h.mu.Unlock() + msg := "Device offline" + switch err { + case hub.ErrTerminalBusy: + msg = "Terminal already open by another viewer" + case hub.ErrDeviceOffline: + msg = "Device offline" + default: + msg = "Failed to start terminal" + h.log.Error("OpenTerminalSession(%s): %v", in.ID, err) + } + c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false, "msg": msg})) + return + } + h.log.Info("term_open: device=%s role=%s", in.ID, c.role) +} + +// handleTermInput forwards xterm.js keystrokes to the device's shell +// sub-conn verbatim. The client's ConPTYManager treats anything that +// isn't a known control byte (CMD_TERMINAL_RESIZE / COMMAND_NEXT) as +// raw PTY input — see client/ConPTYManager.cpp:244. +func (h *wsHub) handleTermInput(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "term_input_result") { + return + } + var in struct { + ID string `json:"id"` + Data string `json:"data"` + } + if err := json.Unmarshal(raw, &in); err != nil { + return + } + if in.ID == "" || in.Data == "" { + return + } + if c.termWatching != in.ID { + return // someone else's session, or no session + } + _ = h.devices.SendToTerminal(in.ID, []byte(in.Data)) +} + +// handleTermResize forwards xterm.js fit/resize events to the device's +// PTY. Legacy cmd-pipe mode silently ignores resize (the underlying +// pipes have no notion of geometry). +func (h *wsHub) handleTermResize(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "term_resize_result") { + return + } + var in struct { + ID string `json:"id"` + Cols int `json:"cols"` + Rows int `json:"rows"` + } + if err := json.Unmarshal(raw, &in); err != nil { + return + } + if in.ID == "" || in.Cols <= 0 || in.Rows <= 0 { + return + } + if c.termWatching != in.ID { + return + } + if !h.devices.TerminalIsPTY(in.ID) { + return // legacy cmd pipe — ignored, same as the C++ guard + } + _ = h.devices.SendToTerminal(in.ID, protocol.BuildTerminalResize(in.Cols, in.Rows)) +} + +// handleTermClose tears down the active session. CloseTerminalSession +// fires OnTerminalClosed which the wsHub broadcast loop turns into the +// front-end's `term_closed` notification — no need to ack here. +func (h *wsHub) handleTermClose(c *wsClient, raw []byte) { + if !h.requireAuth(c, raw, "term_closed") { + return + } + var in struct { + ID string `json:"id"` + } + if err := json.Unmarshal(raw, &in); err != nil || in.ID == "" { + return + } + if c.termWatching != in.ID { + return + } + h.devices.CloseTerminalSession(in.ID) +} + // tickMillis returns a 32-bit-truncated ms timestamp suitable for the // MSG64.time field. The client compares these with GetTickCount(), which // is also a 32-bit ms counter — exact origin doesn't matter, only that -- 2.43.0 From c6244462a99b5ec11496d6269f4955833fb8c6b0 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 18 May 2026 23:37:58 +0200 Subject: [PATCH 7/7] Security(Go): Login rate limit + WS origin allowlist + REST bearer auth --- server/go/README.md | 3 + server/go/cmd/main.go | 47 +++++++++++- server/go/web/server.go | 91 ++++++++++++++++++++--- server/go/web/ws.go | 125 ++++++++++++++++++++++++++++++-- server/go/web/ws_handlers.go | 70 ++++++++++++++++-- server/go/wsauth/ratelimit.go | 110 ++++++++++++++++++++++++++++ server/go/wsauth/wsauth.go | 59 ++++++++++++--- server/go/wsauth/wsauth_test.go | 102 ++++++++++++++++++++++++-- 8 files changed, 566 insertions(+), 41 deletions(-) create mode 100644 server/go/wsauth/ratelimit.go diff --git a/server/go/README.md b/server/go/README.md index dc94308..c32f5ed 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -56,6 +56,7 @@ server/go/ Web 应用能力 (Phase 3-7): - **Web 鉴权**: challenge-response 登录 + 不透明 token,与 users.json schema 互通 +- **登录加固**: 双维度速率限制(10 次/分钟·IP + 5 次/15 分钟·用户名)+ 失败固定延迟,防口令枚举;`/get_salt` 用确定性假盐响应未知用户,杜绝用户名探测;WebSocket Origin 同源校验 + 显式白名单;`/api/devices` Bearer Token 鉴权 - **设备列表与监控**: 在线设备 / RTT / 活动窗口 / 分辨率 实时下发 - **Web 远程桌面**: 浏览器 WebCodecs 解码 H.264,二进制 WS 帧低延迟中继;late-join 自动重发最近 IDR;优雅 BYE 关闭防止客户端无意义重连 - **鼠标 / 键盘输入**: Win32 消息映射 (`WM_*` / `VK_*` / `MK_*`),MSG64 48 字节布局直传客户端 @@ -149,6 +150,8 @@ VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。 | `YAMA_WEB_ADMIN_PASS` | Web UI 的 admin 密码(明文);优先于 `YAMA_PWD`。两者都未设置时 Web 登录禁用 | `your_admin_password` | | `YAMA_SIGN_PASSWORD` | HMAC-SHA256 key used to sign CMD_MASTERSETTING replies; must match the client's expected value. Provision out-of-band. Unset → client refuses screen/file ops. | `` | | `YAMA_USERS_FILE` | Path to the JSON file that persists non-admin web users (allowed_groups, password hash, salt). Default is `users.json` in the working directory. | `users.json` | +| `YAMA_WEB_ALLOWED_ORIGINS` | Comma-separated WebSocket Origin allowlist for cross-origin upgrades. Empty (default) → only same-origin upgrades are accepted, which is correct when the web UI and `/ws` share a host. Add an entry per trusted PWA / dev origin. | `https://yama.example.com,https://yama-mobile.example.com` | +| `YAMA_WEB_TRUST_PROXY` | Set to `1` only when running behind a reverse proxy you control (caddy / nginx / cloudflare). Switches client-IP extraction to use the last entry of `X-Forwarded-For` instead of `RemoteAddr`, so per-IP login rate limit sees the real client. Direct-exposure deployments MUST leave this unset — otherwise attackers can spoof the header to evade rate limits. | `1` | ```bash # Linux/macOS diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index 56d602c..0f75ba5 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -611,6 +611,27 @@ func parsePorts(portStr string) ([]int, error) { return ports, nil } +// splitCSV splits a comma-separated env-var value into trimmed, non-empty +// entries. Returns nil for an empty input so callers can keep the natural +// "no value → no restriction" semantics with a single nil check. +func splitCSV(s string) []string { + if s == "" { + return nil + } + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + out = append(out, p) + } + } + if len(out) == 0 { + return nil + } + return out +} + func main() { // Parse command line flags portStr := flag.String("port", "6543", "Server listen ports (semicolon-separated, e.g. 6543;6544;6545)") @@ -733,9 +754,33 @@ func main() { } } + // Web-UI hardening knobs for public-HTTPS deployment. + // + // YAMA_WEB_ALLOWED_ORIGINS: comma-separated Origin allowlist (e.g. + // "https://yama.example.com,https://yama-mobile.example.com"). + // Empty (default) → only same-origin WS upgrades accepted, which + // is correct when the web UI and WS endpoint share a host. + // + // Login rate limits are hard-coded at sensible defaults for the + // small-user web UI: 10 attempts / minute per IP, 5 / 15 min per + // username. The handler also injects a 250 ms delay on every failure + // so online brute force is impractical even within budget. + allowedOrigins := splitCSV(os.Getenv("YAMA_WEB_ALLOWED_ORIGINS")) + trustProxy := os.Getenv("YAMA_WEB_TRUST_PROXY") == "1" + if trustProxy { + log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front") + } + webCfg := web.Config{ + AllowedOrigins: allowedOrigins, + LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute), + LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute), + TrustForwardedFor: trustProxy, + } + // Start HTTP server for web UI. Hub gives it read-only access to the // device registry; the authenticator owns user accounts and session tokens. - httpSrv := web.New(*httpPort, log.WithPrefix("Web"), deviceHub, webAuth) + httpSrv := web.New(*httpPort, log.WithPrefix("Web"), deviceHub, webAuth). + WithConfig(webCfg) if err := httpSrv.Start(); err != nil { log.Fatal("Failed to start HTTP server: %v", err) } diff --git a/server/go/web/server.go b/server/go/web/server.go index 565de0e..62d9972 100644 --- a/server/go/web/server.go +++ b/server/go/web/server.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "strconv" + "strings" "time" "github.com/yuanyuanxiang/SimpleRemoter/server/go/hub" @@ -19,12 +20,38 @@ import ( // static assets, the PWA manifest, and JSON APIs backed by the device hub. // WebSocket signaling and screen streaming will be wired up in later phases. type Server struct { - port int - log *logger.Logger - srv *http.Server - hub *hub.Hub - auth *wsauth.Authenticator - ws *wsHub + port int + log *logger.Logger + srv *http.Server + hub *hub.Hub + auth *wsauth.Authenticator + ws *wsHub + allowedOrigins []string // for WS Origin allowlist; empty = same-origin only + loginIPLimit *wsauth.RateLimiter + loginUserLimit *wsauth.RateLimiter + trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only) +} + +// Config tunes the server's exposed-on-public-HTTPS hardening knobs. +// All fields are optional; zero values pick reasonable defaults. +type Config struct { + // AllowedOrigins is the comma-separated list of Origin header values + // the WebSocket upgrade will accept in addition to same-origin + // requests. Empty (default) → only same-origin upgrades are allowed, + // which is correct when the web UI and the WS endpoint are served + // from the same host. + AllowedOrigins []string + // LoginIPLimit / LoginUserLimit throttle the get_salt + login flow + // per source IP and per username respectively. Pass nil to disable + // either dimension (e.g. dev mode). + LoginIPLimit *wsauth.RateLimiter + LoginUserLimit *wsauth.RateLimiter + // TrustForwardedFor switches client-IP extraction from RemoteAddr + // (default) to the last entry of X-Forwarded-For. Set true only when + // running behind a reverse proxy that you control; on direct + // exposure the header is client-controlled and would let attackers + // evade per-IP rate limits. + TrustForwardedFor bool } // New creates an HTTP server bound to the given port. port=0 disables the server. @@ -34,6 +61,16 @@ func New(port int, log *logger.Logger, h *hub.Hub, auth *wsauth.Authenticator) * return &Server{port: port, log: log, hub: h, auth: auth} } +// WithConfig applies hardening configuration. Returns the receiver for +// chainable setup. Safe to call before Start; ignored thereafter. +func (s *Server) WithConfig(cfg Config) *Server { + s.allowedOrigins = cfg.AllowedOrigins + s.loginIPLimit = cfg.LoginIPLimit + s.loginUserLimit = cfg.LoginUserLimit + s.trustForwardedFor = cfg.TrustForwardedFor + return s +} + // Start launches the server in a goroutine and returns immediately. // If port is 0, returns nil without starting anything. func (s *Server) Start() error { @@ -42,13 +79,16 @@ func (s *Server) Start() error { return nil } - s.ws = newWSHub(s.auth, s.hub, s.log) + s.ws = newWSHub(s.auth, s.hub, s.log). + withOriginAllowlist(s.allowedOrigins). + withLoginRateLimiters(s.loginIPLimit, s.loginUserLimit). + withTrustForwardedFor(s.trustForwardedFor) mux := http.NewServeMux() mux.HandleFunc("/", s.handleIndex) mux.HandleFunc("/health", s.handleHealth) mux.HandleFunc("/manifest.json", s.handleManifest) - mux.HandleFunc("/api/devices", s.handleDevices) + mux.HandleFunc("/api/devices", s.requireBearer(s.handleDevices)) mux.HandleFunc("/ws", s.ws.serve) mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8")) mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8")) @@ -106,7 +146,7 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { // handleDevices returns a JSON snapshot of currently-online devices. Empty // array (not null) when no clients are connected — matches what the front-end -// will eventually expect. +// will eventually expect. Auth-gated via requireBearer. func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) { devices := s.hub.ListDevices() w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -116,6 +156,39 @@ func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) { } } +// requireBearer wraps a handler with `Authorization: Bearer ` auth +// against the same session-token store the WebSocket uses. Returns 401 on +// missing / invalid / expired tokens. Used to gate REST endpoints that +// previously fell through with no auth (notably /api/devices, which +// otherwise leaks the full online-device list to anyone on the internet). +func (s *Server) requireBearer(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + const prefix = "Bearer " + hdr := r.Header.Get("Authorization") + if !strings.HasPrefix(hdr, prefix) { + s.unauthorized(w) + return + } + token := strings.TrimSpace(hdr[len(prefix):]) + if token == "" { + s.unauthorized(w) + return + } + if _, err := s.auth.ValidateToken(token); err != nil { + s.unauthorized(w) + return + } + next(w, r) + } +} + +func (s *Server) unauthorized(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Bearer realm="yama"`) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) +} + // PWA manifest. Referenced by in index.html. // Static JSON, no template needed. const manifestJSON = `{ diff --git a/server/go/web/ws.go b/server/go/web/ws.go index 5b7b9c5..1328f1a 100644 --- a/server/go/web/ws.go +++ b/server/go/web/ws.go @@ -2,7 +2,10 @@ package web import ( "encoding/json" + "net" "net/http" + "net/url" + "strings" "sync" "time" @@ -20,13 +23,12 @@ const ( wsSendBuffer = 64 // outbound queue depth per client ) -// upgrader allows any origin — this service is meant to be tunneled through -// frp, so requests can legitimately arrive from arbitrary front-end hosts. -// Adjust CheckOrigin once we have a deployment story. -var upgrader = websocket.Upgrader{ +// baseUpgrader carries the buffer-size config shared by all WS upgrades. +// CheckOrigin is set per-hub in wsHub.upgradeWS so the allowlist is +// closed-over per instance instead of being a global mutable. +var baseUpgrader = websocket.Upgrader{ ReadBufferSize: 4096, WriteBufferSize: 4096, - CheckOrigin: func(r *http.Request) bool { return true }, } // ----- per-connection client state ---------------------------------------- @@ -94,6 +96,14 @@ type wsHub struct { clients map[*wsClient]struct{} unsub func() + + // Hardening knobs wired from server.Config. Nil/empty values mean + // "no extra restriction" — useful for local dev where the hub is + // exercised without server.Server wiring up the env-driven defaults. + allowedOrigins []string // empty → only same-origin upgrades accepted + loginIPLimit *wsauth.RateLimiter + loginUserLimit *wsauth.RateLimiter + trustForwardedFor bool // honor X-Forwarded-For (only when behind a trusted proxy) } func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger) *wsHub { @@ -107,6 +117,104 @@ func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger) return h } +// withOriginAllowlist returns h after installing the explicit Origin +// allowlist. Chainable. Pass empty/nil to keep "same-origin only". +func (h *wsHub) withOriginAllowlist(origins []string) *wsHub { + h.allowedOrigins = origins + return h +} + +// withLoginRateLimiters wires per-IP and per-username throttles into +// the login flow. Either may be nil to disable that dimension. +func (h *wsHub) withLoginRateLimiters(byIP, byUser *wsauth.RateLimiter) *wsHub { + h.loginIPLimit = byIP + h.loginUserLimit = byUser + return h +} + +// withTrustForwardedFor opts in to using the last entry of the +// X-Forwarded-For header as the client IP. Safe only when the server is +// behind a reverse proxy that you control. +func (h *wsHub) withTrustForwardedFor(trust bool) *wsHub { + h.trustForwardedFor = trust + return h +} + +// checkOrigin decides whether to accept a WebSocket upgrade based on +// the request's Origin header. Same-origin (Origin host == Host) is +// always accepted; explicit allowlist entries cover the +// PWA-from-different-domain or local-dev cases. +// +// An empty Origin header is rejected: a legitimate browser always sends +// it on cross-origin requests, and same-origin requests have it too in +// modern Chrome/Safari/Firefox. Non-browser clients (curl, scripts) that +// omit Origin shouldn't be talking to the WS endpoint anyway. +func (h *wsHub) checkOrigin(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return false + } + u, err := url.Parse(origin) + if err != nil || u.Host == "" { + return false + } + // Same-origin (Origin host matches the Host the request came in on). + // Strip any port mismatch: if the server is behind a proxy, Host may + // not include a port while Origin does (or vice versa), so compare + // the hostname components. + originHost := u.Hostname() + reqHost := stripPort(r.Host) + if originHost == reqHost && originHost != "" { + return true + } + // Explicit allowlist entries — match Origin in full (scheme + host + // + port) so a customer can pin exactly one trusted PWA origin. + for _, allowed := range h.allowedOrigins { + if allowed == "" { + continue + } + if strings.EqualFold(origin, strings.TrimSpace(allowed)) { + return true + } + } + return false +} + +func stripPort(hostport string) string { + if h, _, err := net.SplitHostPort(hostport); err == nil { + return h + } + return hostport +} + +// clientIP returns the source IP of an HTTP request. By default uses +// r.RemoteAddr (the actual TCP peer); this is the only safe choice when +// the server is directly exposed to the internet, because a malicious +// client can put anything in X-Forwarded-For and would otherwise rotate +// it to evade per-IP rate limits. +// +// When `trustForwardedFor` is true the LAST entry of X-Forwarded-For is +// returned instead — appropriate only when running behind a reverse +// proxy that you control and that overwrites/appends the header (caddy, +// nginx with proper config, etc). Toggled via the YAMA_WEB_TRUST_PROXY +// env var at startup. +func clientIP(r *http.Request, trustForwardedFor bool) string { + if trustForwardedFor { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + // Take the LAST entry — the closest hop, i.e. our own + // trusted proxy's view of the peer. Trusting the first + // entry would let a malicious client at the head of the + // chain set an arbitrary value. + parts := strings.Split(xff, ",") + ip := strings.TrimSpace(parts[len(parts)-1]) + if ip != "" { + return ip + } + } + } + return stripPort(r.RemoteAddr) +} + // stop unsubscribes from the device hub. Existing connections keep running // until they close on their own; we only block new event delivery. func (h *wsHub) stop() { @@ -294,6 +402,11 @@ func (h *wsHub) unregister(c *wsClient) { // ----- HTTP handler ------------------------------------------------------- func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) { + // Build a per-call upgrader so CheckOrigin closes over this hub's + // allowlist instead of a package-level mutable. + upgrader := baseUpgrader + upgrader.CheckOrigin = h.checkOrigin + conn, err := upgrader.Upgrade(w, r, nil) if err != nil { h.log.Error("ws upgrade: %v", err) @@ -313,7 +426,7 @@ func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) { send: make(chan wsMsg, wsSendBuffer), closed: make(chan struct{}), nonce: nonce, - addr: r.RemoteAddr, + addr: clientIP(r, h.trustForwardedFor), } h.register(client) defer h.unregister(client) diff --git a/server/go/web/ws_handlers.go b/server/go/web/ws_handlers.go index f1202fa..8d9f3f7 100644 --- a/server/go/web/ws_handlers.go +++ b/server/go/web/ws_handlers.go @@ -79,18 +79,27 @@ func (h *wsHub) requireAdmin(c *wsClient, raw []byte, replyCmd string) (ok bool) // ----- handlers ------------------------------------------------------------ func (h *wsHub) handleGetSalt(c *wsClient, raw []byte) { + // Throttle the salt-probe surface together with login: an attacker + // who can poll get_salt freely would otherwise still learn nothing + // (the unknown-user fake salt mitigation handles that), but the + // endpoint is otherwise free CPU on the server. Limiting by IP is + // enough; we don't have a username yet to limit by user. + if !h.allowLoginByIP(c) { + // Stall the response so a tight-loop attacker doesn't flood the + // queue. Still return a well-formed salt to avoid making the + // limit detectable from the client side. + time.Sleep(250 * time.Millisecond) + } + var in struct { Username string `json:"username"` } _ = json.Unmarshal(raw, &in) - salt, ok := h.auth.GetSalt(in.Username) - // Do not leak which usernames exist: always return ok=true with a salt. - // For unknown users hand back the empty salt (matches admin convention) - // so the timing/shape of the response is uniform. - if !ok { - salt = "" - } + salt, _ := h.auth.GetSalt(in.Username) + // GetSalt now returns a deterministic fake salt (16 hex chars) for + // unknown users — same shape as a real salt — so an attacker can't + // tell from this response alone whether the username exists. c.queue(mustJSON(map[string]any{ "cmd": "salt", "ok": true, @@ -109,6 +118,20 @@ func (h *wsHub) handleLogin(c *wsClient, raw []byte) { return } + // Rate-limit BEFORE doing the hash work, so a flood doesn't pin CPU. + // Two-dimensional throttle: per-IP catches scanners that try many + // usernames; per-username catches scanners that rotate IPs against a + // known account (admin). Either dimension tripping rejects the call + // with a uniform "credentials" error so the limit is not detectable. + if !h.allowLoginByIP(c) || !h.allowLoginByUsername(in.Username) { + h.log.Warn("ws login throttled: user=%s addr=%s", in.Username, c.addr) + // Burn the challenge so the attacker can't immediately replay. + c.nonce = "" + time.Sleep(500 * time.Millisecond) + c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"})) + return + } + // Bind the response to the challenge we issued at connect time so that // replays from a different connection can't reuse a captured response. if in.Nonce == "" || in.Nonce != c.nonce { @@ -120,9 +143,17 @@ func (h *wsHub) handleLogin(c *wsClient, raw []byte) { if err != nil { // Burn the challenge on failure too — forces a new round on retry. c.nonce = "" + // Fixed delay on failure: makes online brute force impractical + // even within the rate-limit budget, and erases the timing + // difference between "wrong password" and "wrong nonce". + time.Sleep(250 * time.Millisecond) c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"})) return } + // Successful login: clear the per-IP/per-user budgets so a legitimate + // user who fat-fingered a few times doesn't stay throttled. + h.resetLoginThrottle(c, in.Username) + c.nonce = "" c.token = token c.role = role @@ -136,6 +167,31 @@ func (h *wsHub) handleLogin(c *wsClient, raw []byte) { })) } +// allowLoginByIP / allowLoginByUsername return true when the call is +// within budget; nil limiter always returns true (effectively disabled). +func (h *wsHub) allowLoginByIP(c *wsClient) bool { + if h.loginIPLimit == nil || c == nil || c.addr == "" { + return true + } + return h.loginIPLimit.Allow(c.addr) +} + +func (h *wsHub) allowLoginByUsername(username string) bool { + if h.loginUserLimit == nil || username == "" { + return true + } + return h.loginUserLimit.Allow(username) +} + +func (h *wsHub) resetLoginThrottle(c *wsClient, username string) { + if h.loginIPLimit != nil && c != nil && c.addr != "" { + h.loginIPLimit.Reset(c.addr) + } + if h.loginUserLimit != nil && username != "" { + h.loginUserLimit.Reset(username) + } +} + // handleConnect kicks off a screen-sharing session for the browser. We send // COMMAND_SCREEN_SPY to the device's main TCP connection; the device then // opens a new sub-connection (TOKEN_BITMAPINFO) which the TCP side binds to diff --git a/server/go/wsauth/ratelimit.go b/server/go/wsauth/ratelimit.go new file mode 100644 index 0000000..fcaee45 --- /dev/null +++ b/server/go/wsauth/ratelimit.go @@ -0,0 +1,110 @@ +package wsauth + +import ( + "sync" + "time" +) + +// RateLimiter is a sliding-window per-key counter used to throttle login +// attempts. Two instances are typically created: one keyed by client IP +// (to slow distributed brute force), one keyed by username (to slow +// targeted attacks against a known account). +// +// Design notes: +// - Denied attempts are NOT recorded — the window slides naturally and a +// legitimate user who fat-fingers their password recovers as soon as +// the oldest attempt ages out, while a determined attacker is capped +// at `limit` successful attempts per `window` indefinitely. +// - Lazy cleanup: stale timestamps for a key are pruned on every Allow() +// call. Truly idle keys are GC'd by Sweep(), which callers should run +// periodically from a background goroutine. +// - Map size is bounded by the count of recently-active keys; for the +// web UI's expected load (a handful of users + occasional scanners), +// no extra GC pressure considerations needed. +type RateLimiter struct { + mu sync.Mutex + limit int + window time.Duration + entries map[string][]time.Time +} + +// NewRateLimiter returns a limiter that allows up to `limit` events per +// `window` duration per key. Zero or negative limit/window disables the +// limiter (Allow always returns true) — useful for tests / dev mode. +func NewRateLimiter(limit int, window time.Duration) *RateLimiter { + return &RateLimiter{ + limit: limit, + window: window, + entries: make(map[string][]time.Time), + } +} + +// Allow records an attempt for `key` if and only if the caller is under +// the per-key limit. Returns true when allowed, false when over limit. +// Empty key is treated as "no throttle" (returns true without recording) +// so the caller can fall through when the IP/username is unavailable. +func (r *RateLimiter) Allow(key string) bool { + if r == nil || r.limit <= 0 || r.window <= 0 || key == "" { + return true + } + r.mu.Lock() + defer r.mu.Unlock() + + cutoff := time.Now().Add(-r.window) + times := r.entries[key] + // Compact in place — keep only timestamps within the window. + keep := times[:0] + for _, t := range times { + if t.After(cutoff) { + keep = append(keep, t) + } + } + + if len(keep) >= r.limit { + // Update the map even when denying so the compacted slice doesn't + // keep stale entries forever. Don't append the new attempt: that + // would let attackers extend the window arbitrarily. + r.entries[key] = keep + return false + } + + r.entries[key] = append(keep, time.Now()) + return true +} + +// Reset clears state for a key. Call on successful login to give the user +// a fresh budget — otherwise a string of failed attempts followed by a +// correct one still leaves the budget partially consumed. +func (r *RateLimiter) Reset(key string) { + if r == nil || key == "" { + return + } + r.mu.Lock() + delete(r.entries, key) + r.mu.Unlock() +} + +// Sweep removes entries whose timestamps have all aged out of the window. +// Safe to call concurrently with Allow. Intended for periodic invocation +// from a background ticker (e.g. every window-length) to bound the map. +func (r *RateLimiter) Sweep() { + if r == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + cutoff := time.Now().Add(-r.window) + for key, times := range r.entries { + keep := times[:0] + for _, t := range times { + if t.After(cutoff) { + keep = append(keep, t) + } + } + if len(keep) == 0 { + delete(r.entries, key) + } else { + r.entries[key] = keep + } + } +} diff --git a/server/go/wsauth/wsauth.go b/server/go/wsauth/wsauth.go index 15b6b39..10be342 100644 --- a/server/go/wsauth/wsauth.go +++ b/server/go/wsauth/wsauth.go @@ -95,13 +95,26 @@ func (a *Authenticator) AddUser(u User) { a.mu.Unlock() } -// AddAdminFromPlainPassword is a convenience for the bootstrap admin: salt is -// empty (matching the C++ admin record), hash is SHA256(password). +// AddAdminFromPlainPassword is a convenience for the bootstrap admin. +// Unlike legacy convention, the admin record is given a real per-instance +// salt — exposing an empty salt for admin while everyone else has a real +// 16-hex one would let an unauthenticated probe distinguish admin from +// other accounts via /get_salt alone. The cost is a tiny break in +// users.json schema compat: admin is never persisted to users.json +// anyway (snapshotPersistableLocked excludes it), so this is in-memory +// only. func (a *Authenticator) AddAdminFromPlainPassword(username, plainPassword string) { + salt, err := NewSalt() + if err != nil { + // Fall back to deterministic salt derived from the password hash + // rather than empty — preserves the uniform-shape property even + // if crypto/rand briefly errors at startup. + salt = ComputeSHA256(plainPassword)[:saltBytes*2] + } a.AddUser(User{ Username: username, - PasswordHash: ComputeSHA256(plainPassword), - Salt: "", + PasswordHash: HashPassword(plainPassword, salt), + Salt: salt, Role: "admin", }) } @@ -200,17 +213,43 @@ func (a *Authenticator) ListUsers() []User { return out } -// GetSalt returns the per-user salt. If the user does not exist, returns ("", false). -// Note: the C++ admin uses an empty salt — that is still considered "found" -// and the empty string is returned with ok=true. +// GetSalt returns the per-user salt for an existing user, or a +// deterministic 16-hex pseudo-salt for an unknown user. The ok flag +// reports which case occurred, so callers can decide whether to update +// rate-limit / audit state — but the returned salt itself is shaped +// identically (16 hex chars) in both cases, defeating the user-existence +// probe an attacker would otherwise mount via /get_salt. +// +// The pseudo-salt is derived from a server-instance secret (the admin +// password hash, taken at first call) mixed with the username, so the +// same unknown user always sees the same fake salt across requests. +// Without this, an attacker could fingerprint the "fake-salt branch" +// by submitting the same username twice and watching for differences. func (a *Authenticator) GetSalt(username string) (string, bool) { a.mu.RLock() u, ok := a.users[username] a.mu.RUnlock() - if !ok { - return "", false + if ok { + return u.Salt, true } - return u.Salt, true + return a.fakeSalt(username), false +} + +// fakeSalt derives a deterministic 16-hex value for unknown usernames. +// The secret pepper is the bootstrap admin's password hash — present as +// long as the server has any admin, deterministic per deployment, never +// transmitted. Reveals nothing useful to an attacker even if reverse- +// engineered: the only thing they can do with it is reproduce the fake +// salt, which they already see in the response. +func (a *Authenticator) fakeSalt(username string) string { + a.mu.RLock() + pepper := "" + if admin, ok := a.users["admin"]; ok { + pepper = admin.PasswordHash + } + a.mu.RUnlock() + digest := ComputeSHA256("yama-fake-salt|" + pepper + "|" + username) + return digest[:saltBytes*2] } // VerifyLogin checks a challenge-response login. The browser sends diff --git a/server/go/wsauth/wsauth_test.go b/server/go/wsauth/wsauth_test.go index 6e0b7cd..78c1f24 100644 --- a/server/go/wsauth/wsauth_test.go +++ b/server/go/wsauth/wsauth_test.go @@ -14,19 +14,31 @@ func TestSHA256Vector(t *testing.T) { } } -func TestLoginRoundTripAdminEmptySalt(t *testing.T) { +// adminLoginResponse helps tests compute the right login response for an +// admin account that now uses a real per-instance salt. +func adminLoginResponse(t *testing.T, a *Authenticator, username, password, nonce string) string { + t.Helper() + salt, ok := a.GetSalt(username) + if !ok { + t.Fatalf("admin %s not registered", username) + } + return ComputeSHA256(HashPassword(password, salt) + nonce) +} + +func TestLoginRoundTripAdmin(t *testing.T) { a := New() a.AddAdminFromPlainPassword("admin", "hunter2") salt, ok := a.GetSalt("admin") - if !ok || salt != "" { - t.Fatalf("admin salt: ok=%v salt=%q", ok, salt) + if !ok { + t.Fatal("admin should be found") + } + if len(salt) != 2*saltBytes { + t.Fatalf("admin salt should be a real 16-hex value, got %q (len=%d)", salt, len(salt)) } - // Simulate the browser: nonce = "abc123", response = SHA256(passwordHash + nonce) nonce := "abc123" - passwordHash := ComputeSHA256("hunter2") - response := ComputeSHA256(passwordHash + nonce) + response := adminLoginResponse(t, a, "admin", "hunter2", nonce) token, role, err := a.VerifyLogin("admin", response, nonce) if err != nil { @@ -71,6 +83,33 @@ func TestLoginRoundTripViewerWithSalt(t *testing.T) { } } +// TestGetSaltUnknownUserShape verifies the salt-probe mitigation: an +// unknown user must get back a value that's shape-identical to a real +// salt, so an attacker can't tell from /get_salt alone whether a +// username exists. +func TestGetSaltUnknownUserShape(t *testing.T) { + a := New() + a.AddAdminFromPlainPassword("admin", "pw") + + fake, ok := a.GetSalt("nobody") + if ok { + t.Fatal("ok should be false for unknown user") + } + if len(fake) != 2*saltBytes { + t.Fatalf("fake salt should be %d hex chars; got %q (len=%d)", 2*saltBytes, fake, len(fake)) + } + // Determinism: repeated probes for the same username get the same fake. + fake2, _ := a.GetSalt("nobody") + if fake != fake2 { + t.Fatalf("fake salt should be deterministic for repeated probes; got %q vs %q", fake, fake2) + } + // Different usernames get different fake salts. + other, _ := a.GetSalt("ghost") + if fake == other { + t.Fatalf("fake salts should differ across usernames; both = %q", fake) + } +} + func TestLoginRejectsWrongResponse(t *testing.T) { a := New() a.AddAdminFromPlainPassword("admin", "x") @@ -89,7 +128,7 @@ func TestTokenExpiry(t *testing.T) { a.SetTokenExpire(50 * time.Millisecond) a.AddAdminFromPlainPassword("admin", "x") nonce, _ := NewNonce() - response := ComputeSHA256(ComputeSHA256("x") + nonce) + response := adminLoginResponse(t, a, "admin", "x", nonce) token, _, err := a.VerifyLogin("admin", response, nonce) if err != nil { t.Fatal(err) @@ -107,10 +146,57 @@ func TestRevoke(t *testing.T) { a := New() a.AddAdminFromPlainPassword("admin", "x") nonce, _ := NewNonce() - response := ComputeSHA256(ComputeSHA256("x") + nonce) + response := adminLoginResponse(t, a, "admin", "x", nonce) token, _, _ := a.VerifyLogin("admin", response, nonce) a.RevokeToken(token) if _, err := a.ValidateToken(token); err == nil { t.Fatal("revoked token should not validate") } } + +func TestRateLimiterAllowsBurstThenBlocks(t *testing.T) { + r := NewRateLimiter(3, time.Minute) + for i := 0; i < 3; i++ { + if !r.Allow("ip-a") { + t.Fatalf("attempt %d should be allowed", i+1) + } + } + if r.Allow("ip-a") { + t.Fatal("4th attempt should be denied") + } + // Different key has independent budget. + if !r.Allow("ip-b") { + t.Fatal("different key should still be allowed") + } +} + +func TestRateLimiterReset(t *testing.T) { + r := NewRateLimiter(2, time.Minute) + r.Allow("k") + r.Allow("k") + if r.Allow("k") { + t.Fatal("3rd should be denied") + } + r.Reset("k") + if !r.Allow("k") { + t.Fatal("after Reset, should be allowed again") + } +} + +func TestRateLimiterDisabledWhenZeroLimit(t *testing.T) { + r := NewRateLimiter(0, time.Minute) + for i := 0; i < 100; i++ { + if !r.Allow("k") { + t.Fatalf("limit=0 should never deny, denied at i=%d", i) + } + } +} + +func TestRateLimiterNilSafe(t *testing.T) { + var r *RateLimiter + if !r.Allow("anything") { + t.Fatal("nil limiter should allow") + } + r.Reset("anything") + r.Sweep() +} -- 2.43.0