Merge branch 'pr-6' into test

# Conflicts:
#	frontend/src/terminal.ts
#	src/worker/durable-object.ts
#	src/worker/html.ts
#	src/worker/ssh-session.ts
This commit is contained in:
TanXin committed 2026-06-23 21:35:59 +08:00
commit 8db135b3b5
5 files changed
+145 -34

No files matched your search

+23 -7
View File
@@ -152,6 +152,7 @@ export class SSHTerminal {
password: config.password,
authMethod: config.authMethod,
privateKey: config.privateKey,
...this.getTerminalSize(),
}));
this.startHeartbeat();
@@ -188,9 +189,14 @@ export class SSHTerminal {
ws.onopen = () => {
this.terminal.writeln('\x1b[32m[+] WebSocket connected, authenticating...\x1b[0m');
this.sendResize();
this.startHeartbeat();
};
if (ws.readyState === WebSocket.OPEN) {
this.sendResize();
}
this.setupWebSocketHandlers();
}
@@ -284,13 +290,7 @@ export class SSHTerminal {
// Terminal resize: send to server + update trzsz column count
this.disposables.push(
this.terminal.onResize(({ cols, rows }) => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'resize',
cols,
rows,
}));
}
this.sendResize({ cols, rows });
this.trzszFilter?.setTerminalColumns(cols);
})
);
@@ -309,6 +309,22 @@ export class SSHTerminal {
}, 30000);
}
private getTerminalSize(): { cols: number; rows: number } {
return {
cols: this.terminal.cols,
rows: this.terminal.rows,
};
}
private sendResize(size = this.getTerminalSize()): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'resize',
...size,
}));
}
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
+7
View File
@@ -46,6 +46,13 @@ export interface SSHConnectionConfig {
password: string;
authMethod?: 'password' | 'publickey';
privateKey?: string;
cols?: number;
rows?: number;
}
export interface TerminalSize {
cols: number;
rows: number;
}
export interface Env {
+65 -13
View File
@@ -1,4 +1,4 @@
import { Env, SSHConnectionConfig } from '../types';
import { Env, SSHConnectionConfig, TerminalSize } from '../types';
import { SSHSession } from './ssh-session';
/**
@@ -38,6 +38,7 @@ export class SSHSessionDO {
private env: Env;
private sessions: Map<WebSocket, SSHSession> = new Map();
private _pendingTimeouts: Map<WebSocket, ReturnType<typeof setTimeout>> = new Map();
private pendingTerminalSizes: Map<WebSocket, TerminalSize> = new Map();
constructor(state: DurableObjectState, env: Env) {
this.state = state;
@@ -105,26 +106,43 @@ export class SSHSessionDO {
return;
}
if (typeof message !== 'string') {
return;
}
let msg: any;
try {
msg = JSON.parse(message);
} catch {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid credentials format' }));
ws.close(1011, 'Invalid format');
return;
}
if (msg.type === 'resize') {
this.rememberTerminalSize(ws, msg.cols, msg.rows);
return;
}
if (msg.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
const timeout = this._pendingTimeouts.get(ws);
if (timeout) {
clearTimeout(timeout);
this._pendingTimeouts.delete(ws);
}
try {
const config = JSON.parse(message as string) as SSHConnectionConfig;
const config = msg as SSHConnectionConfig;
if (!config.host || !config.username || (!config.password && !config.privateKey)) {
ws.send(JSON.stringify({ type: 'error', message: 'Missing credentials' }));
ws.close(1011, 'Invalid credentials');
return;
}
await this.initSSHSession(ws, config);
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid credentials format' }));
ws.close(1011, 'Invalid format');
if (!config.host || !config.username || (!config.password && !config.privateKey)) {
ws.send(JSON.stringify({ type: 'error', message: 'Missing credentials' }));
ws.close(1011, 'Invalid credentials');
return;
}
await this.initSSHSession(ws, config);
}
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
@@ -138,6 +156,7 @@ export class SSHSessionDO {
clearTimeout(timeout);
this._pendingTimeouts.delete(ws);
}
this.pendingTerminalSizes.delete(ws);
}
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
@@ -165,8 +184,14 @@ export class SSHSessionDO {
await socket.opened;
const strictVerify = this.env.STRICT_HOST_KEY_VERIFY !== 'false';
const pendingSize = this.pendingTerminalSizes.get(ws);
if (pendingSize) {
config.cols = pendingSize.cols;
config.rows = pendingSize.rows;
}
const session = new SSHSession(ws, socket, config, strictVerify);
this.sessions.set(ws, session);
this.pendingTerminalSizes.delete(ws);
await session.startHandshake();
@@ -179,4 +204,31 @@ export class SSHSessionDO {
} catch {}
}
}
private rememberTerminalSize(ws: WebSocket, cols: unknown, rows: unknown): void {
const size = this.normalizeTerminalSize(cols, rows);
if (size) this.pendingTerminalSizes.set(ws, size);
}
private normalizeTerminalSize(cols: unknown, rows: unknown): TerminalSize | null {
if (
typeof cols !== 'number' ||
typeof rows !== 'number' ||
!Number.isFinite(cols) ||
!Number.isFinite(rows)
) {
return null;
}
const size = {
cols: Math.floor(cols),
rows: Math.floor(rows),
};
if (size.cols < 10 || size.cols > 1000 || size.rows < 5 || size.rows > 1000) {
return null;
}
return size;
}
}
+4 -2
View File
@@ -4,7 +4,7 @@ export const HTML = `<!DOCTYPE html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+CiAgPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzEzMTMxMyIvPgogIDxyZWN0IHg9IjIiIHk9IjIiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgcng9IjMiIGZpbGw9IiMxYTFhMWEiIHN0cm9rZT0iIzRhZjYyNiIgc3Ryb2tlLXdpZHRoPSIxIi8+CiAgPHBhdGggZD0iTTggMTFMMTQgMTZMOCAyMSIgc3Ryb2tlPSIjNGFmNjI2IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+CiAgPGxpbmUgeDE9IjE2IiB5MT0iMjEiIHgyPSIyNCIgeTI9IjIxIiBzdHJva2U9IiM0YWY2MjYiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+DQogIDxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgcng9IjQiIGZpbGw9IiMxMzEzMTMiLz4NCiAgPHJlY3QgeD0iMiIgeT0iMiIgd2lkdGg9IjI4IiBoZWlnaHQ9IjI4IiByeD0iMyIgZmlsbD0iIzFhMWExYSIgc3Ryb2tlPSIjNGFmNjI2IiBzdHJva2Utd2lkdGg9IjEiLz4NCiAgPHBhdGggZD0iTTggMTFMMTQgMTZMOCAyMSIgc3Ryb2tlPSIjNGFmNjI2IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+DQogIDxsaW5lIHgxPSIxNiIgeTE9IjIxIiB4Mj0iMjQiIHkyPSIyMSIgc3Ryb2tlPSIjNGFmNjI2IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+DQo8L3N2Zz4=" />
<title>CloudSSH</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
@@ -40,6 +40,7 @@ export const HTML = `<!DOCTYPE html>
}
}
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module">var ao=Object.defineProperty;var ho=(i,e,t)=>e in i?ao(i,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):i[e]=t;var Q=(i,e,t)=>ho(i,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))r(n);new MutationObserver(n=>{for(const o of n)if(o.type==="childList")for(const _ of o.addedNodes)_.tagName==="LINK"&&_.rel==="modulepreload"&&r(_)}).observe(document,{childList:!0,subtree:!0});function t(n){const o={};return n.integrity&&(o.integrity=n.integrity),n.referrerPolicy&&(o.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?o.credentials="include":n.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(n){if(n.ep)return;n.ep=!0;const o=t(n);fetch(n.href,o)}})();var Hi={exports:{}},$s;function lo(){return $s||($s=1,(function(i,e){(function(t,r){i.exports=r()})(globalThis,(()=>(()=>{var t={4567:function(_,s,h){var a=this&&this.__decorate||function(l,c,v,y){var S,x=arguments.length,b=x<3?c:y===null?y=Object.getOwnPropertyDescriptor(c,v):y;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")b=Reflect.decorate(l,c,v,y);else for(var E=l.length-1;E>=0;E--)(S=l[E])&&(b=(x<3?S(b):x>3?S(c,v,b):S(c,v))||b);return x>3&&b&&Object.defineProperty(c,v,b),b},d=this&&this.__param||function(l,c){return function(v,y){c(v,y,l)}};Object.defineProperty(s,"__esModule",{value:!0}),s.AccessibilityManager=void 0;const u=h(9042),p=h(9924),m=h(844),C=h(4725),w=h(2585),f=h(3656);let g=s.AccessibilityManager=class extends m.Disposable{constructor(l,c,v,y){super(),this._terminal=l,this._coreBrowserService=v,this._renderService=y,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let S=0;S<this._terminal.rows;S++)this._rowElements[S]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[S]);if(this._topBoundaryFocusListener=S=>this._handleBoundaryFocus(S,0),this._bottomBoundaryFocusListener=S=>this._handleBoundaryFocus(S,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=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new p.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((S=>this._handleResize(S.rows)))),this.register(this._terminal.onRender((S=>this._refreshRows(S.start,S.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((S=>this._handleChar(S)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\`
\`)))),this.register(this._terminal.onA11yTab((S=>this._handleTab(S)))),this.register(this._terminal.onKey((S=>this._handleKey(S.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,f.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,m.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(l){for(let c=0;c<l;c++)this._handleChar(" ")}_handleChar(l){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==l&&(this._charsToAnnounce+=l):this._charsToAnnounce+=l,l===\`
@@ -132,7 +133,7 @@ void main() {
\`),process.stdout.write("\\x1B[H\\x1B[2J\\x1B[?1049l")):process.stdout.write("\\x1B8\\x1B[0J"),process.stdout.write(e),process.stdout.write(\`\\r
\`),process.stdout.write("\\x1B[?25h"),this.transferConfig.tmux_output_junk&&(yield(function(){return q(this,void 0,void 0,function*(){yield require("util").promisify(require("child_process").exec)("tmux refresh-client")})})())})}deleteCreatedFiles(){return q(this,void 0,void 0,function*(){const e=[];for(const t of this.createdFiles){const r=yield t.deleteFile();r&&e.push(r)}return e})}clientError(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(this.cleanTimeoutInMilliseconds);const t=X.getErrorMessage(e);let r=!0;if(e instanceof X){if(r=e.isTraceBack(),e.isRemoteExit())return;if(e.isRemoteFail())return void(r&&console.log(t))}yield this.sendString(r?"FAIL":"fail",t),r&&console.log(t)})}serverError(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(this.cleanTimeoutInMilliseconds);const t=X.getErrorMessage(e);let r=!0;if(e instanceof X){if(e.isStopAndDelete()){const n=yield this.deleteCreatedFiles();if(n&&n.length)return void(yield this.serverExit([e.message+":"].concat(n).join(\`\\r
- \`)))}if(r=e.isTraceBack(),e.isRemoteExit()||e.isRemoteFail())return void(yield this.serverExit(t))}yield this.sendString(r?"FAIL":"fail",t),yield this.serverExit(t)})}sendFileNum(e,t){return q(this,void 0,void 0,function*(){yield this.sendInteger("NUM",e),yield this.checkInteger(e),t&&t.onNum(e)})}sendFileName(e,t,r){return q(this,void 0,void 0,function*(){const n=e.getRelPath(),o=n[n.length-1];if(t){const s={path_id:e.getPathId(),path_name:n,is_dir:e.isDir()};yield this.sendString("NAME",JSON.stringify(s))}else yield this.sendString("NAME",o);const _=yield this.recvString("SUCC");return r&&r.onName(o),_})}sendFileSize(e,t){return q(this,void 0,void 0,function*(){yield this.sendInteger("SIZE",e),yield this.checkInteger(e),t&&t.onSize(e)})}sendFileData(e,t,r,n,o,_){return q(this,void 0,void 0,function*(){let s=0;_&&_.onStep(s);let h=1024,a=new ArrayBuffer(h);const d=new hi;for(;s<t;){const u=Date.now(),p=yield e.readFile(a);yield this.sendData(p,r,n),d.appendByteArray(p),yield this.checkInteger(p.length),s+=p.length,_&&_.onStep(s);const m=Date.now()-u;p.length==h&&m<500&&h<o?(h=Math.min(2*h,o),a=new ArrayBuffer(h)):m>=2e3&&h>1024&&(h=1024,a=new ArrayBuffer(h)),m>this.maxChunkTimeInMilliseconds&&(this.maxChunkTimeInMilliseconds=m)}return new Uint8Array(d.end(!0).buffer)})}sendFileMD5(e,t){return q(this,void 0,void 0,function*(){yield this.sendBinary("MD5",e),yield this.checkBinary(e),t&&t.onDone()})}sendFiles(e,t){return q(this,void 0,void 0,function*(){this.openedFiles.push(...e);const r=this.transferConfig.binary===!0,n=this.transferConfig.directory===!0,o=this.transferConfig.bufsize?Math.min(this.transferConfig.bufsize,this.maxDataChunkSize):this.maxDataChunkSize,_=this.transferConfig.escape_chars?qr(this.transferConfig.escape_chars):[];yield this.sendFileNum(e.length,t);const s=[];for(const h of e){const a=yield this.sendFileName(h,n,t);if(s.includes(a)||s.push(a),h.isDir())continue;const d=h.getSize();yield this.sendFileSize(d,t);const u=yield this.sendFileData(h,d,r,_,o,t);h.closeFile(),yield this.sendFileMD5(u,t)}return s})}recvFileNum(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("NUM");return yield this.sendInteger("SUCC",t),e&&e.onNum(t),t})}recvFileName(e,t,r,n,o){return q(this,void 0,void 0,function*(){const _=yield this.recvString("NAME"),s=yield t(e,_,r,n);return this.createdFiles.push(s),yield this.sendString("SUCC",s.getLocalName()),o&&o.onName(s.getFileName()),s})}recvFileSize(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("SIZE");return yield this.sendInteger("SUCC",t),e&&e.onSize(t),t})}recvFileData(e,t,r,n,o,_){return q(this,void 0,void 0,function*(){let s=0;_&&_.onStep(s);const h=new hi;for(;s<t;){const a=Date.now(),d=yield this.recvData(r,n,o);yield e.writeFile(d),s+=d.length,_&&_.onStep(s),yield this.sendInteger("SUCC",d.length),h.appendByteArray(d);const u=Date.now()-a;u>this.maxChunkTimeInMilliseconds&&(this.maxChunkTimeInMilliseconds=u)}return new Uint8Array(h.end(!0).buffer)})}recvFileMD5(e,t){return q(this,void 0,void 0,function*(){const r=yield this.recvBinary("MD5");if(e.length!=r.length)throw new X("Check MD5 failed");for(let n=0;n<e.length;n++)if(e[n]!=r[n])throw new X("Check MD5 failed");yield this.sendBinary("SUCC",e),t&&t.onDone()})}recvFiles(e,t,r){return q(this,void 0,void 0,function*(){const n=this.transferConfig.binary===!0,o=this.transferConfig.directory===!0,_=this.transferConfig.overwrite===!0,s=this.transferConfig.timeout?1e3*this.transferConfig.timeout:1e5,h=this.transferConfig.escape_chars?qr(this.transferConfig.escape_chars):[],a=yield this.recvFileNum(r),d=[];for(let u=0;u<a;u++){const p=yield this.recvFileName(e,t,o,_,r);if(d.includes(p.getLocalName())||d.push(p.getLocalName()),p.isDir())continue;this.openedFiles.push(p);const m=yield this.recvFileSize(r),C=yield this.recvFileData(p,m,n,h,s,r);p.closeFile(),yield this.recvFileMD5(C,r)}return d})}}function Ci(i,e){e-=3;let t=0,r="";for(let n=0;n<i.length;n++){if(i.charCodeAt(n)>=19968&&i.charCodeAt(n)<=40869){if(t+2>e)return{sub:r+"...",len:t+3};t+=2}else{if(t+1>e)return{sub:r+"...",len:t+3};t+=1}r+=i[n]}return{sub:r+"...",len:t+3}}function Kr(i){let e="B";do{if(i<1024||(e="KB",(i/=1024)<1024)||(e="MB",(i/=1024)<1024)||(e="GB",(i/=1024)<1024))break;i/=1024,e="TB"}while(!1);return i>=100?\`\${i.toFixed(0)} \${e}\`:i>=10?\`\${i.toFixed(1)} \${e}\`:\`\${i.toFixed(2)} \${e}\`}Oi.MAX_DATA_CHUNK_SIZE=10485760;class ol{constructor(e,t,r=void 0){this.lastUpdateTime=0,this.firstWrite=!0,this.speedCnt=0,this.speedIdx=0,this.timeArray=new Array(30),this.stepArray=new Array(30),this.writer=e,this.tmuxPaneColumns=r||0,this.columns=this.tmuxPaneColumns>1?this.tmuxPaneColumns-1:t}setTerminalColumns(e){this.columns=e,this.tmuxPaneColumns>0&&(this.tmuxPaneColumns=0)}onNum(e){this.fileCount=e,this.fileIdx=0}onName(e){this.fileName=e,this.fileIdx+=1,this.startTime=Date.now(),this.timeArray[0]=this.startTime,this.stepArray[0]=0,this.speedCnt=1,this.speedIdx=1,this.fileStep=- Line truncated
\`)}setTimeout(()=>this.detectAndHandleTrzsz(e),10),this.writeToTerminal(e)}}processTerminalInput(e){this.isTransferringFiles()?e===""&&this.stopTransferringFiles():this.sendToServer(e)}processBinaryInput(e){this.isTransferringFiles()||this.sendToServer(Ti(e))}setTerminalColumns(e){this.terminalColumns=e,this.textProgressBar!=null&&this.textProgressBar.setTerminalColumns(e)}isTransferringFiles(){return this.trzszTransfer!=null}stopTransferringFiles(){this.isTransferringFiles()&&this.trzszTransfer.stopTransferring()}uploadFiles(e){return q(this,void 0,void 0,function*(){if(this.uploadFilesList||this.isTransferringFiles())throw new Error("The previous upload has not been completed yet");if(!Ut&&(function(r,n){if(!Array.isArray(r))return!1;for(const o of r)if(typeof o!==n)return!1;return!0})(e,"string"))this.uploadFilesList=yield Hr(e,!0);else{if(!(typeof DataTransferItemList<"u"&&e instanceof DataTransferItemList))throw new Error("The upload items type is not supported");this.uploadFilesList=yield(function(r){return q(this,void 0,void 0,function*(){const n=[],o=[];for(const _ of r)o.push(_.webkitGetAsEntry());for(let _=0;_<o.length;_++){const s=o[_];s&&(yield io(_,s,n,[s.name]))}return n})})(e)}if(!this.uploadFilesList||!this.uploadFilesList.length)throw this.uploadFilesList=null,new Error("No files to upload");let t=!1;for(const r of this.uploadFilesList)if(r.isDir()||r.getRelPath().length>1){t=!0;break}return this.uploadInterrupting=!0,this.sendToServer(""),yield new Promise(r=>setTimeout(r,200)),this.uploadInterrupting=!1,this.uploadSkipTrzCommand=!0,this.sendToServer(t?"trz -d\\r":"trz\\r"),setTimeout(()=>{this.uploadFilesList&&(this.uploadFilesList=null,this.uploadFilesResolve=null,this.uploadFilesReject&&(this.uploadFilesReject("Upload does not start"),this.uploadFilesReject=null))},this.dragInitTimeout),new Promise((r,n)=>{this.uploadFilesResolve=r,this.uploadFilesReject=n})})}uniqueIdExists(e){if(e.length<8||!this.isWindowsShell&&e.length==14&&e.endsWith("00"))return!1;if(this.uniqueIdMaps.has(e))return!0;if(this.uniqueIdMaps.size>=100){const t=new Map;for(const[r,n]of this.uniqueIdMaps)n>=50&&t.set(r,n-50);this.uniqueIdMaps=t}return this.uniqueIdMaps.set(e,this.uniqueIdMaps.size),!1}detectAndHandleTrzsz(e){return q(this,void 0,void 0,function*(){const t=yield(function(h){return q(this,void 0,void 0,function*(){if(typeof h=="string"){const p=h.lastIndexOf(so);return p<0?null:h.substring(p)}let a;if(h instanceof ArrayBuffer)a=new Uint8Array(h);else if(h instanceof Uint8Array)a=h;else{if(!(h instanceof Blob))return null;a=new Uint8Array(yield h.arrayBuffer())}if(a.length<26)return null;let d=-1,u=-1;for(;;){if(d=a.indexOf(58,d+1),d<0||a.length-d<26)return u>=0?Di(a.subarray(u)):null;const p=new Float64Array(a.buffer.slice(a.byteOffset+d,a.byteOffset+d+16));p[0]==Vr[0]&&p[1]==Vr[1]&&(u=d,d+=25)}})})(e);if(!t)return;const r=t.match(al);if(!r)return;const n=r.length>3?r[3]:"";if(this.uniqueIdExists(n))return;const o=r[1],_=r[2];let s=!1;(n==":1"||n.length==14&&n.endsWith("10"))&&(s=!0);try{this.trzszTransfer=new Oi(this.sendToServer,this.isWindowsShell,this.maxDataChunkSize),o==="S"?yield this.handleTrzszDownloadFiles(_,s):o==="R"?yield this.handleTrzszUploadFiles(_,!1,s):o==="D"&&(yield this.handleTrzszUploadFiles(_,!0,s)),this.uploadFilesResolve&&this.uploadFilesResolve()}catch(h){yield this.trzszTransfer.clientError(h),this.uploadFilesReject&&this.uploadFilesReject(h)}finally{this.uploadFilesResolve=null,this.uploadFilesReject=null,this.trzszTransfer.cleanup(),this.textProgressBar&&this.textProgressBar.showCursor(),this.textProgressBar=null,this.trzszTransfer=null}})}createProgressBar(e,t){e!==!0?(this.textProgressBar=new ol(this.writeToTerminal,this.terminalColumns,t),this.textProgressBar.hideCursor()):this.textProgressBar=null}handleTrzszDownloadFiles(e,t){return q(this,void 0,void 0,function*(){let r,n,o;if(Ut){const h=yield(function(){return q(this,void 0,void 0,function*(){if(typeof window.showDirectoryPicker!="function")throw Bs();try{return yield window.showDirectoryPicker({id:"trzsz_download",startIn:"downloads",mode:"readwrite"})}catch(a){if(a.name==="AbortError")return;throw a}})})();if(!h)return void(yield this.trzszTransfer.sendAction(!1,t));r=h.name,n={handle:h,maps:new Map},o=il}else{if(r=yield this.chooseSaveDirectory(),!r)return void(yield this.trzszTransfer.sendAction(!1,t));yield(function(h){return q(this,void 0,void 0,function*(){if(!h)return!1;if(!(yield te.accessAsync(h)))throw new X(\`No such directory: \${h}\`);if(!(yield te.statAsync(h)).isDirectory())throw new X(\`Not a directory: \${h}\`);if(!(yield te.accessAsync(h,te.constants.W_OK)))throw new X(\`No permission to write: \${h}\`);return!0})})(r),n={path:r,maps:new Map},o=el}yield this.trzszTransfer.sendAction(!0,t);const _=yield this.trzszTransfer.recvConfig();this.createProgressBar(_.quiet,_.tmux_pane_width);const s=yield this.trzszTransfer.recvFiles(n,o,this.textProgressBar);yield this.trzszTransfer.clientExit(Fr(s,r))}) Line truncated
\`)}setTimeout(()=>this.detectAndHandleTrzsz(e),10),this.writeToTerminal(e)}}processTerminalInput(e){this.isTransferringFiles()?e===""&&this.stopTransferringFiles():this.sendToServer(e)}processBinaryInput(e){this.isTransferringFiles()||this.sendToServer(Ti(e))}setTerminalColumns(e){this.terminalColumns=e,this.textProgressBar!=null&&this.textProgressBar.setTerminalColumns(e)}isTransferringFiles(){return this.trzszTransfer!=null}stopTransferringFiles(){this.isTransferringFiles()&&this.trzszTransfer.stopTransferring()}uploadFiles(e){return q(this,void 0,void 0,function*(){if(this.uploadFilesList||this.isTransferringFiles())throw new Error("The previous upload has not been completed yet");if(!Ut&&(function(r,n){if(!Array.isArray(r))return!1;for(const o of r)if(typeof o!==n)return!1;return!0})(e,"string"))this.uploadFilesList=yield Hr(e,!0);else{if(!(typeof DataTransferItemList<"u"&&e instanceof DataTransferItemList))throw new Error("The upload items type is not supported");this.uploadFilesList=yield(function(r){return q(this,void 0,void 0,function*(){const n=[],o=[];for(const _ of r)o.push(_.webkitGetAsEntry());for(let _=0;_<o.length;_++){const s=o[_];s&&(yield io(_,s,n,[s.name]))}return n})})(e)}if(!this.uploadFilesList||!this.uploadFilesList.length)throw this.uploadFilesList=null,new Error("No files to upload");let t=!1;for(const r of this.uploadFilesList)if(r.isDir()||r.getRelPath().length>1){t=!0;break}return this.uploadInterrupting=!0,this.sendToServer(""),yield new Promise(r=>setTimeout(r,200)),this.uploadInterrupting=!1,this.uploadSkipTrzCommand=!0,this.sendToServer(t?"trz -d\\r":"trz\\r"),setTimeout(()=>{this.uploadFilesList&&(this.uploadFilesList=null,this.uploadFilesResolve=null,this.uploadFilesReject&&(this.uploadFilesReject("Upload does not start"),this.uploadFilesReject=null))},this.dragInitTimeout),new Promise((r,n)=>{this.uploadFilesResolve=r,this.uploadFilesReject=n})})}uniqueIdExists(e){if(e.length<8||!this.isWindowsShell&&e.length==14&&e.endsWith("00"))return!1;if(this.uniqueIdMaps.has(e))return!0;if(this.uniqueIdMaps.size>=100){const t=new Map;for(const[r,n]of this.uniqueIdMaps)n>=50&&t.set(r,n-50);this.uniqueIdMaps=t}return this.uniqueIdMaps.set(e,this.uniqueIdMaps.size),!1}detectAndHandleTrzsz(e){return q(this,void 0,void 0,function*(){const t=yield(function(h){return q(this,void 0,void 0,function*(){if(typeof h=="string"){const p=h.lastIndexOf(so);return p<0?null:h.substring(p)}let a;if(h instanceof ArrayBuffer)a=new Uint8Array(h);else if(h instanceof Uint8Array)a=h;else{if(!(h instanceof Blob))return null;a=new Uint8Array(yield h.arrayBuffer())}if(a.length<26)return null;let d=-1,u=-1;for(;;){if(d=a.indexOf(58,d+1),d<0||a.length-d<26)return u>=0?Di(a.subarray(u)):null;const p=new Float64Array(a.buffer.slice(a.byteOffset+d,a.byteOffset+d+16));p[0]==Vr[0]&&p[1]==Vr[1]&&(u=d,d+=25)}})})(e);if(!t)return;const r=t.match(al);if(!r)return;const n=r.length>3?r[3]:"";if(this.uniqueIdExists(n))return;const o=r[1],_=r[2];let s=!1;(n==":1"||n.length==14&&n.endsWith("10"))&&(s=!0);try{this.trzszTransfer=new Oi(this.sendToServer,this.isWindowsShell,this.maxDataChunkSize),o==="S"?yield this.handleTrzszDownloadFiles(_,s):o==="R"?yield this.handleTrzszUploadFiles(_,!1,s):o==="D"&&(yield this.handleTrzszUploadFiles(_,!0,s)),this.uploadFilesResolve&&this.uploadFilesResolve()}catch(h){yield this.trzszTransfer.clientError(h),this.uploadFilesReject&&this.uploadFilesReject(h)}finally{this.uploadFilesResolve=null,this.uploadFilesReject=null,this.trzszTransfer.cleanup(),this.textProgressBar&&this.textProgressBar.showCursor(),this.textProgressBar=null,this.trzszTransfer=null}})}createProgressBar(e,t){e!==!0?(this.textProgressBar=new ol(this.writeToTerminal,this.terminalColumns,t),this.textProgressBar.hideCursor()):this.textProgressBar=null}handleTrzszDownloadFiles(e,t){return q(this,void 0,void 0,function*(){let r,n,o;if(Ut){const h=yield(function(){return q(this,void 0,void 0,function*(){if(typeof window.showDirectoryPicker!="function")throw Bs();try{return yield window.showDirectoryPicker({id:"trzsz_download",startIn:"downloads",mode:"readwrite"})}catch(a){if(a.name==="AbortError")return;throw a}})})();if(!h)return void(yield this.trzszTransfer.sendAction(!1,t));r=h.name,n={handle:h,maps:new Map},o=il}else{if(r=yield this.chooseSaveDirectory(),!r)return void(yield this.trzszTransfer.sendAction(!1,t));yield(function(h){return q(this,void 0,void 0,function*(){if(!h)return!1;if(!(yield te.accessAsync(h)))throw new X(\`No such directory: \${h}\`);if(!(yield te.statAsync(h)).isDirectory())throw new X(\`Not a directory: \${h}\`);if(!(yield te.accessAsync(h,te.constants.W_OK)))throw new X(\`No permission to write: \${h}\`);return!0})})(r),n={path:r,maps:new Map},o=el}yield this.trzszTransfer.sendAction(!0,t);const _=yield this.trzszTransfer.recvConfig();this.createProgressBar(_.quiet,_.tmux_pane_width);const s=yield this.trzszTransfer.recvFiles(n,o,this.textProgressBar);yield this.trzszTransfer.clientExit(Fr(s,r))}) Line truncated
<button type="button" id="github-login-btn" class="github-login-btn text-[11px] font-bold tracking-[0.1em] text-[#bbccb0] hover:text-[#4af626] transition-all cursor-pointer flex items-center gap-1.5 bg-transparent border border-[#3c4b36] px-3 py-1 hover:border-[#4af626]">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
LOGIN
@@ -494,6 +495,7 @@ void main() {
</footer>
</div>
</div>
</body>
</html>
`;
+46 -12
View File
@@ -1,4 +1,4 @@
import { SSHConnectionConfig, SessionKeys, SSHPacket } from '../types';
import { SSHConnectionConfig, SessionKeys, SSHPacket, TerminalSize } from '../types';
import {
SSH_MSG_KEXINIT,
SSH_MSG_NEWKEYS,
@@ -83,6 +83,7 @@ export class SSHSession {
private keepaliveInterval: ReturnType<typeof setInterval> | null = null;
private shellReadyTimeout: ReturnType<typeof setTimeout> | null = null;
private terminalSize: TerminalSize = { cols: 120, rows: 40 };
constructor(
ws: WebSocket,
@@ -98,6 +99,7 @@ export class SSHSession {
this.transport = new SSHTransport();
this.packetParser = new SSHPacketParser();
this.channel = new SSHChannel();
this.updateTerminalSize(config.cols, config.rows);
}
async startHandshake(): Promise<void> {
@@ -842,7 +844,7 @@ export class SSHSession {
switch (msgType) {
case SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
this.channel.handleOpenConfirmation(payload);
const ptyReq = this.channel.buildPTYRequest(120, 40);
const ptyReq = this.channel.buildPTYRequest(this.terminalSize.cols, this.terminalSize.rows);
await this.sendEncrypted(ptyReq);
break;
@@ -922,38 +924,70 @@ export class SSHSession {
}
async handleWebSocketMessage(data: string | ArrayBuffer): Promise<void> {
if (this.state !== 'ready') return;
if (typeof data === 'string') {
if (data === '{"type":"ping"}') {
this.ws.send(JSON.stringify({ type: 'pong' }));
return;
}
if (data.startsWith('{"type":"resize"')) {
if (data.startsWith('{"type"')) {
try {
const msg = JSON.parse(data);
if (msg.type === 'ping') {
this.ws.send(JSON.stringify({ type: 'pong' }));
return;
}
if (msg.type === 'resize') {
await this.handleResize(msg.cols, msg.rows);
return;
}
} catch {}
}
if (this.state !== 'ready') return;
const encoded = new TextEncoder().encode(data);
const channelData = this.channel.buildChannelData(encoded);
await this.sendEncrypted(channelData);
} else {
if (this.state !== 'ready') return;
const channelData = this.channel.buildChannelData(new Uint8Array(data));
await this.sendEncrypted(channelData);
}
}
private async handleResize(cols: number, rows: number): Promise<void> {
const resizeMsg = this.channel.buildWindowChange(cols, rows);
private async handleResize(cols: unknown, rows: unknown): Promise<void> {
if (!this.updateTerminalSize(cols, rows)) return;
if (this.state !== 'ready') return;
const resizeMsg = this.channel.buildWindowChange(this.terminalSize.cols, this.terminalSize.rows);
await this.sendEncrypted(resizeMsg);
}
private updateTerminalSize(cols: unknown, rows: unknown): boolean {
if (
typeof cols !== 'number' ||
typeof rows !== 'number' ||
!Number.isFinite(cols) ||
!Number.isFinite(rows)
) {
return false;
}
const nextSize = {
cols: Math.floor(cols),
rows: Math.floor(rows),
};
if (
nextSize.cols < 10 ||
nextSize.cols > 1000 ||
nextSize.rows < 5 ||
nextSize.rows > 1000
) {
return false;
}
this.terminalSize = nextSize;
return true;
}
private async sendEncrypted(payload: Uint8Array): Promise<void> {
const operation = this.sendMutex.then(async () => {
const encrypted = await this.buildEncryptedPacket(payload);