diff --git a/frontend/src/terminal.ts b/frontend/src/terminal.ts index 949cd4b..6f6c0bd 100644 --- a/frontend/src/terminal.ts +++ b/frontend/src/terminal.ts @@ -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); diff --git a/src/types.ts b/src/types.ts index 2d4ac6f..90f2721 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 { diff --git a/src/worker/durable-object.ts b/src/worker/durable-object.ts index dcecb9b..c15725c 100644 --- a/src/worker/durable-object.ts +++ b/src/worker/durable-object.ts @@ -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 = new Map(); private _pendingTimeouts: Map> = new Map(); + private pendingTerminalSizes: Map = 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 { @@ -138,6 +156,7 @@ export class SSHSessionDO { clearTimeout(timeout); this._pendingTimeouts.delete(ws); } + this.pendingTerminalSizes.delete(ws); } async webSocketError(ws: WebSocket, error: unknown): Promise { @@ -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; + } } diff --git a/src/worker/html.ts b/src/worker/html.ts index 8a15870..442ce27 100644 --- a/src/worker/html.ts +++ b/src/worker/html.ts @@ -4,7 +4,7 @@ export const HTML = ` - + CloudSSH @@ -40,6 +40,7 @@ export const HTML = ` } } +