Files
CloudSSH/src/worker/ssh-session.ts
T

447 lines
14 KiB
TypeScript

import { SSHConnectionConfig } from '../types';
import {
SSH_MSG_KEXINIT,
SSH_MSG_NEWKEYS,
SSH_MSG_KEX_ECDH_REPLY,
SSH_MSG_USERAUTH_SUCCESS,
SSH_MSG_USERAUTH_FAILURE,
SSH_MSG_CHANNEL_OPEN_CONFIRMATION,
SSH_MSG_CHANNEL_SUCCESS,
SSH_MSG_CHANNEL_DATA,
SSH_MSG_CHANNEL_WINDOW_ADJUST,
SSH_MSG_CHANNEL_EOF,
SSH_MSG_CHANNEL_CLOSE,
SSH_MSG_DISCONNECT,
SSH_MSG_IGNORE,
SSH_MSG_DEBUG
} from '../types';
import { SSHTransport } from '../ssh/transport';
import { SSHPacketParser, SSHPacketBuilder } from '../ssh/packet';
import { KEXInitBuilder } from '../ssh/kex';
import { ECDHKeyExchange } from '../ssh/kex-ecdh';
import { KeyDerivation } from '../ssh/keys';
import { SSHAESGCMCipher } from '../ssh/crypto';
import { SSHAuth } from '../ssh/auth';
import { SSHChannel } from '../ssh/channel';
export class SSHSession {
private ws: WebSocket;
private socket: any;
private config: SSHConnectionConfig;
private transport: SSHTransport;
private packetParser: SSHPacketParser;
private channel: SSHChannel;
private encryptCipher: SSHAESGCMCipher | null = null;
private decryptCipher: SSHAESGCMCipher | null = null;
private derivedKeys: any = null;
private seqNumSend: number = 0;
private seqNumRecv: number = 0;
private sessionID: Uint8Array | null = null;
private kexInitLocal: Uint8Array | null = null;
private kexInitRemote: Uint8Array | null = null;
private ecdhKeyPair!: CryptoKeyPair;
private ecdhRawPublicKey!: Uint8Array;
private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'ready'
= 'connecting';
constructor(ws: WebSocket, socket: any, config: SSHConnectionConfig) {
console.log('[SSH] Constructor called');
this.ws = ws;
this.socket = socket;
this.config = config;
this.transport = new SSHTransport();
this.packetParser = new SSHPacketParser();
this.channel = new SSHChannel();
}
async startHandshake(): Promise<void> {
console.log('[SSH] Starting handshake, current state:', this.state);
this.state = 'version';
console.log('[SSH] State changed to: version');
const writer = this.socket.writable.getWriter();
const versionStr = 'SSH-2.0-CloudSSH_1.0\r\n';
console.log('[SSH] Sending version string:', versionStr.trim());
await writer.write(new TextEncoder().encode(versionStr));
writer.releaseLock();
console.log('[SSH] Version string sent');
this.startReading();
}
private async startReading(): Promise<void> {
console.log('[SSH] Starting read loop');
const reader = this.socket.readable.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('[SSH] TCP stream ended');
break;
}
console.log('[SSH] Received data, state:', this.state, 'bytes:', value.length);
if (this.state === 'version') {
const versionStr = decoder.decode(value);
console.log('[SSH] Server version:', versionStr.trim());
if (this.transport.handleVersionExchange(versionStr)) {
console.log('[SSH] Version exchange complete');
this.state = 'kex';
console.log('[SSH] State changed to: kex');
await this.startKEX();
}
} else {
try {
this.packetParser.feed(value);
await this.processPackets();
} catch (pktError) {
const pktErrMsg = pktError instanceof Error ? pktError.message : String(pktError);
console.error('[SSH] Packet processing error:', pktErrMsg);
throw pktError;
}
}
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
const errStack = error instanceof Error ? error.stack : '';
console.error('[SSH] Read loop error:', errMsg, errStack);
try {
this.ws.send(JSON.stringify({
type: 'error',
message: 'SSH 连接断开: ' + errMsg
}));
} catch {}
}
}
private async startKEX(): Promise<void> {
console.log('[KEX] Starting key exchange');
this.kexInitLocal = KEXInitBuilder.build();
console.log('[KEX] Built KEXINIT, length:', this.kexInitLocal.length);
const packet = await SSHPacketBuilder.build(
this.kexInitLocal, 8, null, this.seqNumSend++
);
console.log('[KEX] Sending KEXINIT packet, length:', packet.length);
await this.writeSocket(packet);
console.log('[KEX] KEXINIT sent');
console.log('[KEX] Generating ECDH key pair...');
this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair();
console.log('[KEX] ECDH key pair generated');
this.ecdhRawPublicKey = await ECDHKeyExchange.exportRawPublicKey(
this.ecdhKeyPair
);
console.log('[KEX] Raw public key exported, length:', this.ecdhRawPublicKey.length);
const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhRawPublicKey);
const ecdhPacket = await SSHPacketBuilder.build(
ecdhInit, 8, null, this.seqNumSend++
);
console.log('[KEX] Sending ECDH_INIT packet, length:', ecdhPacket.length);
await this.writeSocket(ecdhPacket);
console.log('[KEX] ECDH_INIT sent, waiting for server reply...');
}
private async writeSocket(data: Uint8Array): Promise<void> {
const writer = this.socket.writable.getWriter();
await writer.write(data);
writer.releaseLock();
}
private async processPackets(): Promise<void> {
const blockSize = this.decryptCipher ? 16 : 8;
while (true) {
const packet = await this.packetParser.nextPacket(
blockSize,
this.decryptCipher
? (data, seq) => this.decryptCipher!.decrypt(data, seq)
: (data) => data,
!!this.decryptCipher
);
if (!packet) break;
const msgType = packet.payload[0];
console.log('[PKT] Received message type:', msgType, 'state:', this.state);
await this.handlePacket(packet);
}
}
private async handlePacket(packet: any): Promise<void> {
const msgType = packet.payload[0];
switch (this.state) {
case 'kex':
await this.handleKEXPacket(msgType, packet.payload);
break;
case 'auth':
await this.handleAuthPacket(msgType, packet.payload);
break;
case 'shell':
case 'ready':
await this.handleSessionPacket(msgType, packet.payload);
break;
}
}
private async handleKEXPacket(msgType: number, payload: Uint8Array): Promise<void> {
switch (msgType) {
case SSH_MSG_KEXINIT:
console.log('[KEX] Received KEXINIT from server');
this.kexInitRemote = payload;
break;
case SSH_MSG_KEX_ECDH_REPLY:
console.log('[KEX] Received ECDH_REPLY');
await this.handleECDHReply(payload);
break;
case SSH_MSG_NEWKEYS:
console.log('[KEX] Received NEWKEYS from server');
const newKeys = new Uint8Array([SSH_MSG_NEWKEYS]);
const packet = await SSHPacketBuilder.build(
newKeys, 8, null, this.seqNumSend++
);
await this.writeSocket(packet);
console.log('[KEX] NEWKEYS sent');
this.seqNumSend = 0;
this.packetParser.resetSeqNum();
await this.enableEncryption();
console.log('[KEX] Encryption enabled');
this.state = 'auth';
console.log('[SSH] State changed to: auth');
await this.authenticate();
break;
}
}
private async handleECDHReply(payload: Uint8Array): Promise<void> {
console.log('[KEX] Parsing ECDH_REPLY...');
const { hostKey, serverRawPublicKey, signature } =
ECDHKeyExchange.parseReply(payload);
console.log('[KEX] ECDH_REPLY parsed, hostKey:', hostKey.length, 'serverPubKey:', serverRawPublicKey.length, 'sig:', signature.length);
console.log('[KEX] Computing shared secret...');
const sharedSecret = await ECDHKeyExchange.computeSharedSecret(
this.ecdhKeyPair.privateKey,
serverRawPublicKey
);
console.log('[KEX] Shared secret computed, length:', sharedSecret.length);
console.log('[KEX] Computing exchange hash...');
const H = await ECDHKeyExchange.computeExchangeHash(
this.transport.getLocalVersion(),
this.transport.getRemoteVersion(),
this.kexInitLocal!,
this.kexInitRemote!,
hostKey,
this.ecdhRawPublicKey,
serverRawPublicKey,
sharedSecret
);
console.log('[KEX] Exchange hash computed');
if (!this.sessionID) {
this.sessionID = H;
console.log('[KEX] Session ID set');
}
console.log('[KEX] Deriving keys...');
this.derivedKeys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!);
console.log('[KEX] Keys derived, waiting for NEWKEYS...');
}
private async enableEncryption(): Promise<void> {
console.log('[KEX] Enabling encryption...');
const keys = this.derivedKeys;
this.encryptCipher = new SSHAESGCMCipher(
keys.encKeyClientToServer,
keys.ivClientToServer
);
await this.encryptCipher.init();
this.decryptCipher = new SSHAESGCMCipher(
keys.encKeyServerToClient,
keys.ivServerToClient
);
await this.decryptCipher.init();
console.log('[KEX] Encryption ciphers ready');
}
private async authenticate(): Promise<void> {
console.log('[AUTH] Sending password authentication...');
const authRequest = SSHAuth.buildPasswordAuthRequest(
this.config.username,
this.config.password
);
console.log('[AUTH] Auth request built, length:', authRequest.length);
const packet = await SSHPacketBuilder.build(
authRequest, 16,
(data, seq) => this.encryptCipher!.encrypt(data, seq),
this.seqNumSend++,
true
);
await this.writeSocket(packet);
console.log('[AUTH] Auth request sent');
}
private async handleAuthPacket(msgType: number, payload: Uint8Array): Promise<void> {
switch (msgType) {
case SSH_MSG_USERAUTH_SUCCESS:
console.log('[AUTH] Authentication successful!');
this.ws.send(JSON.stringify({
type: 'status',
message: '认证成功'
}));
this.state = 'shell';
console.log('[SSH] State changed to: shell');
await this.openShell();
break;
case SSH_MSG_USERAUTH_FAILURE:
console.log('[AUTH] Authentication failed');
this.ws.send(JSON.stringify({
type: 'error',
message: '认证失败:用户名或密码错误'
}));
this.close();
break;
}
}
private async openShell(): Promise<void> {
console.log('[SHELL] Opening session channel...');
const openMsg = this.channel.buildOpenSession();
await this.sendEncrypted(openMsg);
console.log('[SHELL] Channel open request sent');
}
private async handleSessionPacket(msgType: number, payload: Uint8Array): Promise<void> {
console.log('[SESSION] Received message type:', msgType);
switch (msgType) {
case SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
console.log('[SESSION] Channel opened successfully');
this.channel.handleOpenConfirmation(payload);
console.log('[SESSION] Sending PTY request...');
const ptyReq = this.channel.buildPTYRequest(120, 40);
await this.sendEncrypted(ptyReq);
console.log('[SESSION] PTY request sent');
break;
case SSH_MSG_CHANNEL_SUCCESS:
console.log('[SESSION] Channel success, state:', this.state);
if (this.state === 'shell') {
console.log('[SESSION] Sending shell request...');
const shellReq = this.channel.buildShellRequest();
await this.sendEncrypted(shellReq);
this.state = 'ready';
console.log('[SSH] State changed to: ready - SHELL ACTIVE!');
}
break;
case SSH_MSG_CHANNEL_DATA:
const outputData = this.channel.handleChannelData(payload);
this.ws.send(outputData.slice().buffer);
break;
case SSH_MSG_CHANNEL_WINDOW_ADJUST:
console.log('[SESSION] Window adjust');
break;
case SSH_MSG_CHANNEL_EOF:
case SSH_MSG_CHANNEL_CLOSE:
console.log('[SESSION] Channel closed');
this.ws.send(JSON.stringify({
type: 'status',
message: '会话已结束'
}));
this.close();
break;
case SSH_MSG_DISCONNECT:
console.log('[SESSION] Server disconnected');
this.ws.send(JSON.stringify({
type: 'status',
message: '服务器断开连接'
}));
this.close();
break;
case SSH_MSG_IGNORE:
case SSH_MSG_DEBUG:
break;
}
}
handleWebSocketMessage(data: string | ArrayBuffer): void {
if (this.state !== 'ready') {
console.log('[INPUT] Ignoring input, state:', this.state);
return;
}
if (typeof data === 'string') {
try {
const msg = JSON.parse(data);
if (msg.type === 'resize') {
console.log('[INPUT] Terminal resize:', msg.cols, 'x', msg.rows);
this.handleResize(msg.cols, msg.rows);
return;
}
} catch {
const encoded = new TextEncoder().encode(data);
const channelData = this.channel.buildChannelData(encoded);
this.sendEncrypted(channelData);
}
} else {
const channelData = this.channel.buildChannelData(
new Uint8Array(data)
);
this.sendEncrypted(channelData);
}
}
private async handleResize(cols: number, rows: number): Promise<void> {
const resizeMsg = this.channel.buildWindowChange(cols, rows);
await this.sendEncrypted(resizeMsg);
}
private async sendEncrypted(payload: Uint8Array): Promise<void> {
if (!this.encryptCipher) {
throw new Error('Encryption not initialized');
}
const encrypted = await SSHPacketBuilder.build(
payload, 16,
(data, seq) => this.encryptCipher!.encrypt(data, seq),
this.seqNumSend++,
true
);
await this.writeSocket(encrypted);
}
close(): void {
console.log('[SSH] Closing session');
try { this.socket.close(); } catch {}
try { this.ws.close(); } catch {}
}
}