"use strict";
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.SshProtocol = void 0;
const buffer_1 = require("buffer");
const queue_1 = require("../util/queue");
const semaphore_1 = require("../util/semaphore");
const sshMessage_1 = require("../messages/sshMessage");
const sshData_1 = require("../io/sshData");
const transportMessages_1 = require("../messages/transportMessages");
const kexMessages_1 = require("../messages/kexMessages");
const connectionMessages_1 = require("../messages/connectionMessages");
const errors_1 = require("../errors");
const trace_1 = require("../trace");
class SequencedMessage {
    constructor(sequence, message) {
        this.sequence = sequence;
        this.message = message;
    }
}
/**
 * Implements the base SSH protocol (sending and receiving messages) over a Stream.
 */
class SshProtocol {
    constructor(stream, config, metrics, trace) {
        this.config = config;
        this.metrics = metrics;
        this.trace = trace;
        this.sessionSemaphore = new semaphore_1.Semaphore(1);
        this.inboundPacketSequence = 0;
        this.outboundPacketSequence = 0;
        this.inboundFlow = 0;
        this.outboundFlow = 0;
        // Sent messages are kept for a short time, until the other side acknowledges
        // that they have been received. This enables re-sending lost messages on reconnect.
        this.recentSentMessages = new queue_1.Queue();
        // Initialize buffers that are re-used for each sent/received message.
        // The buffers will be automatically expanded as necessary.
        this.sendWriter = new sshData_1.SshDataWriter(buffer_1.Buffer.alloc(1024));
        this.receiveWriter = new sshData_1.SshDataWriter(buffer_1.Buffer.alloc(1024));
        /* @internal */
        this.traceChannelData = false;
        this.extensions = null;
        this.kexService = null;
        this.algorithms = null;
        this.messageContext = null;
        this.outgoingMessagesHaveLatencyInfo = false;
        this.incomingMessagesHaveLatencyInfo = false;
        this.outgoingMessagesHaveReconnectInfo = false;
        this.incomingMessagesHaveReconnectInfo = false;
        this.stream = stream;
        this.traceChannelData = config.traceChannelData;
    }
    get lastIncomingSequence() {
        return this.inboundPacketSequence - 1;
    }
    getSentMessages(startingSequenceNumber) {
        if (startingSequenceNumber === this.outboundPacketSequence + 1) {
            // The recipient is already up-to-date.
            return [];
        }
        if (this.recentSentMessages.size > 0 &&
            startingSequenceNumber < this.recentSentMessages.peek().sequence) {
            // The cached recent messages do not go back as far as the requested sequence number.
            // This should never happen because messages are not dropped from this list until
            // the other side acknowledges they have been received, so they should not be
            // requested again after reconnecting.
            return null;
        }
        // Return all messages starting with the requested sequence number.
        // Exclude key exchange messages because they cannot be retransmitted; a reconnected
        // session will do key exchange separately. Also exclude any disconnect messages that
        // may have been attempted when the connection was lost.
        const messagesToRetransmit = new Array();
        for (const sequencedMessage of this.recentSentMessages) {
            if (sequencedMessage.sequence >= startingSequenceNumber) {
                const message = sequencedMessage.message;
                if (!(message instanceof kexMessages_1.KeyExchangeMessage || message instanceof transportMessages_1.DisconnectMessage)) {
                    messagesToRetransmit.push(message);
                }
            }
        }
        return messagesToRetransmit;
    }
    async writeProtocolVersion(version, cancellation) {
        const stream = this.stream;
        if (!stream)
            throw new Error('SSH session disconnected.');
        const data = buffer_1.Buffer.from(version + '\r\n');
        await stream.write(data, cancellation);
        this.metrics.addMessageSent(data.length);
        return Promise.resolve();
    }
    async readProtocolVersion(cancellation) {
        const stream = this.stream;
        if (!stream)
            throw new Error('SSH session disconnected.');
        // http://tools.ietf.org/html/rfc4253#section-4.2
        const buffer = buffer_1.Buffer.alloc(255);
        let lineCount = 0;
        for (let i = 0; i < buffer.length; i++) {
            const byteBuffer = await stream.read(1, cancellation);
            if (!byteBuffer) {
                break;
            }
            buffer[i] = byteBuffer[0];
            const carriageReturn = 0x0d;
            const lineFeed = 0x0a;
            if (i > 0 && buffer[i - 1] === carriageReturn && buffer[i] === lineFeed) {
                const line = buffer.toString('utf8', 0, i - 1);
                if (line.startsWith('SSH-')) {
                    this.metrics.addMessageReceived(i + 1);
                    return line;
                }
                else if (lineCount > 20) {
                    // Give up if a version string was not found after 20 lines.
                    break;
                }
                else {
                    // Ignore initial lines before the version line.
                    lineCount++;
                    i = -1;
                }
            }
        }
        throw new errors_1.SshConnectionError('Failed to read the protocol version', transportMessages_1.SshDisconnectReason.protocolError);
    }
    async handleNewKeys(cancellation) {
        try {
            await this.sessionSemaphore.wait(cancellation);
            this.inboundFlow = 0;
            this.outboundFlow = 0;
            this.algorithms = this.kexService.finishKeyExchange();
        }
        finally {
            this.sessionSemaphore.release();
        }
    }
    /**
     * Attempts to read from the stream until the buffer is full.
     * @returns True if the read succeeded, false if the stream was disposed.
     */
    async read(buffer, cancellation) {
        const stream = this.stream;
        if (!stream)
            return false;
        let bytesRead = 0;
        do {
            let data;
            try {
                data = await stream.read(buffer.length - bytesRead, cancellation);
            }
            catch (e) {
                if (!(e instanceof Error))
                    throw e;
                if (stream.isDisposed)
                    return false;
                stream.dispose();
                this.stream = null;
                this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamReadError, `Error reading from stream: ${e.message}`, e);
                throw new errors_1.SshConnectionError('Error reading from stream: ' + e.message, transportMessages_1.SshDisconnectReason.connectionLost);
            }
            if (!data)
                return false;
            data.copy(buffer, bytesRead);
            bytesRead += data.length;
        } while (bytesRead < buffer.length);
        return true;
    }
    /**
     * Attempts to write data to the stream.
     * @returns True if the write succeeded, false if the stream was disposed.
     */
    async write(data, cancellation) {
        const stream = this.stream;
        if (!stream)
            return false;
        try {
            await stream.write(data, cancellation);
        }
        catch (e) {
            if (!(e instanceof Error))
                throw e;
            if (stream.isDisposed)
                return false;
            stream.dispose();
            this.stream = null;
            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamWriteError, `Error writing to stream: ${e.message}`, e);
            throw new errors_1.SshConnectionError('Error writing to stream: ' + e.message, transportMessages_1.SshDisconnectReason.connectionLost);
        }
        return true;
    }
    async considerReExchange(initial, cancellation) {
        const kexService = this.kexService;
        if (!kexService)
            return;
        let kexMessage = null;
        let kexGuessMessage = null;
        if (!kexService.exchanging &&
            (initial || this.inboundFlow + this.outboundFlow > this.config.keyRotationThreshold)) {
            [kexMessage, kexGuessMessage] = await kexService.startKeyExchange(initial);
        }
        if (kexMessage) {
            await this.sendMessage(kexMessage, cancellation);
            if (kexGuessMessage) {
                await this.sendMessage(kexGuessMessage, cancellation);
            }
        }
    }
    async computeHmac(signer, payload, seq) {
        const writer = new sshData_1.SshDataWriter(buffer_1.Buffer.alloc(4 + payload.length));
        writer.writeUInt32(seq);
        writer.write(payload);
        const result = await signer.sign(writer.toBuffer());
        return result;
    }
    async verifyHmac(verifier, payload, seq, mac) {
        const writer = new sshData_1.SshDataWriter(buffer_1.Buffer.alloc(4 + payload.length));
        writer.writeUInt32(seq);
        writer.write(payload);
        const result = await verifier.verify(writer.toBuffer(), mac);
        return result;
    }
    async readAndVerifyHmac(verifier, data, macBuffer, cancellation) {
        if (!(await this.read(macBuffer, cancellation))) {
            return false;
        }
        const verified = await this.verifyHmac(verifier, data, this.inboundPacketSequence, macBuffer);
        if (!verified) {
            throw new errors_1.SshConnectionError('Invalid MAC', transportMessages_1.SshDisconnectReason.macError);
        }
        return true;
    }
    /**
     * Attemps to write one message to the stream.
     * @returns `true` if writing succeeded, `false` if the stream was disposed.
     * @throws SshConnectionException if writing to the stream failed for any other reason.
     */
    async sendMessage(message, cancellation) {
        var _a;
        const algorithms = this.algorithms;
        const compression = algorithms === null || algorithms === void 0 ? void 0 : algorithms.compressor;
        const encryption = algorithms === null || algorithms === void 0 ? void 0 : algorithms.cipher;
        const hmac = algorithms === null || algorithms === void 0 ? void 0 : algorithms.messageSigner;
        let result;
        await this.sessionSemaphore.wait(cancellation);
        try {
            const blockSize = encryption ? Math.max(8, encryption.blockLength) : 8;
            // Start by writing the uncompressed payload to the buffer at the correct offset.
            const payloadOffset = SshProtocol.packetLengthSize + SshProtocol.paddingLengthSize;
            this.sendWriter.position = payloadOffset;
            message.write(this.sendWriter);
            if (this.outgoingMessagesHaveReconnectInfo) {
                // Write the sequence number of the last inbound packet processed.
                this.sendWriter.writeUInt64(this.lastIncomingSequence);
                if (this.outgoingMessagesHaveLatencyInfo) {
                    // Write the time (in microseconds, not ms) since last packet was received.
                    const timeSinceLastReceivedMessage = Math.min(4294967295, // max uint32
                    Math.round((this.metrics.time - this.lastIncomingTimestamp) * 1000));
                    this.sendWriter.writeUInt32(timeSinceLastReceivedMessage);
                }
            }
            let payload = this.sendWriter.toBuffer().slice(payloadOffset);
            if (compression != null) {
                payload = compression.compress(payload);
            }
            // The packet length is not encrypted when in EtM or AEAD mode.
            const isLengthEncrypted = !((hmac === null || hmac === void 0 ? void 0 : hmac.encryptThenMac) || (hmac === null || hmac === void 0 ? void 0 : hmac.authenticatedEncryption));
            // http://tools.ietf.org/html/rfc4253
            // 6.  Binary Packet Protocol
            // the total length of (packet_length || padding_length || payload || padding)
            // is a multiple of the cipher block size or 8,
            // padding length must between 4 and 255 bytes.
            let paddingLength = blockSize -
                (((isLengthEncrypted ? SshProtocol.packetLengthSize : 0) +
                    SshProtocol.paddingLengthSize +
                    payload.length) %
                    blockSize);
            if (paddingLength < 4) {
                paddingLength += blockSize;
            }
            const packetLength = SshProtocol.paddingLengthSize + payload.length + paddingLength;
            this.sendWriter.position = 0;
            this.sendWriter.writeUInt32(packetLength);
            this.sendWriter.writeByte(paddingLength);
            // The uncompressed payload was already written at the correct offset.
            // When compression is enabled, rewrite the compressed payload.
            if (compression != null) {
                this.sendWriter.write(payload);
            }
            else {
                this.sendWriter.position += payload.length;
            }
            this.sendWriter.writeRandom(paddingLength);
            payload = this.sendWriter.toBuffer();
            let mac = null;
            if ((hmac === null || hmac === void 0 ? void 0 : hmac.encryptThenMac) && encryption) {
                // In EtM mode, compute the MAC after encrypting. And don't encrypt the length.
                const packetWithoutLength = payload.slice(SshProtocol.packetLengthSize, payload.length);
                const encryptedPacket = await encryption.transform(packetWithoutLength);
                encryptedPacket.copy(packetWithoutLength);
                mac = await this.computeHmac(hmac, payload, this.outboundPacketSequence);
            }
            else if (hmac === null || hmac === void 0 ? void 0 : hmac.authenticatedEncryption) {
                // With a GCM cipher, the packet length is not included in the plaintext.
                const packetWithoutLength = payload.slice(SshProtocol.packetLengthSize, payload.length);
                const encryptedPacket = await encryption.transform(packetWithoutLength);
                encryptedPacket.copy(packetWithoutLength);
                // The GCM tag was already generated during the transform call above;
                // this just retrieves it.
                mac = await hmac.sign(packetWithoutLength);
            }
            else {
                if (hmac) {
                    mac = await this.computeHmac(hmac, payload, this.outboundPacketSequence);
                }
                if (encryption) {
                    payload = await encryption.transform(payload);
                }
            }
            if (!(message instanceof connectionMessages_1.ChannelDataMessage)) {
                this.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.sendingMessage, `Sending #${this.outboundPacketSequence} ${message}`);
            }
            else if (this.traceChannelData) {
                this.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.sendingChannelData, `Sending #${this.outboundPacketSequence} ${message}`);
            }
            if (this.incomingMessagesHaveReconnectInfo) {
                // Save sent messages in case they need to be re-sent after reconnect.
                // They'll be discarded soon, after the other side acknowledges them.
                const sequencedMessage = new SequencedMessage(this.outboundPacketSequence, message);
                sequencedMessage.sentTime = this.metrics.time;
                this.recentSentMessages.enqueue(sequencedMessage);
            }
            this.outboundPacketSequence++;
            this.outboundFlow += packetLength;
            if (mac) {
                const packet = buffer_1.Buffer.concat([payload, mac], payload.length + mac.length);
                result = await this.write(packet, cancellation);
            }
            else {
                result = await this.write(payload, cancellation);
            }
            this.metrics.addMessageSent(SshProtocol.packetLengthSize + packetLength + ((_a = hmac === null || hmac === void 0 ? void 0 : hmac.digestLength) !== null && _a !== void 0 ? _a : 0));
        }
        finally {
            this.sessionSemaphore.release();
        }
        await this.considerReExchange(false, cancellation);
        return result;
    }
    /**
     * Attemps to read one message from the stream.
     * @returns The message, or `null` if the stream was disposed.
     * @throws SshConnectionError if reading from the stream failed for any other reason.
     */
    async receiveMessage(cancellation) {
        var _a;
        const algorithms = this.algorithms;
        const encryption = algorithms === null || algorithms === void 0 ? void 0 : algorithms.decipher;
        const hmac = algorithms === null || algorithms === void 0 ? void 0 : algorithms.messageVerifier;
        const compression = algorithms === null || algorithms === void 0 ? void 0 : algorithms.decompressor;
        // The packet length is not encrypted when in EtM or AEAD mode.
        // So read only the length bytes first, separate from the remaining payload.
        const isLengthEncrypted = !((hmac === null || hmac === void 0 ? void 0 : hmac.encryptThenMac) || (hmac === null || hmac === void 0 ? void 0 : hmac.authenticatedEncryption));
        const firstBlockSize = !isLengthEncrypted
            ? SshProtocol.packetLengthSize
            : encryption
                ? Math.max(8, encryption.blockLength)
                : 8;
        this.receiveWriter.position = firstBlockSize;
        let firstBlock = this.receiveWriter.toBuffer();
        if (!(await this.read(firstBlock, cancellation))) {
            return null;
        }
        this.lastIncomingTimestamp = this.metrics.time;
        // Decrypt the first block to get the packet length.
        if (encryption && isLengthEncrypted) {
            firstBlock = await encryption.transform(firstBlock);
            this.receiveWriter.position = 0;
            this.receiveWriter.write(firstBlock);
        }
        const receiveReader = new sshData_1.SshDataReader(firstBlock);
        const packetLength = receiveReader.readUInt32();
        if (packetLength > SshProtocol.maxPacketLength) {
            throw new errors_1.SshConnectionError('Invalid packet length.', transportMessages_1.SshDisconnectReason.protocolError);
        }
        const packetBufferSize = SshProtocol.packetLengthSize + packetLength;
        if (packetBufferSize > firstBlockSize) {
            this.receiveWriter.skip(packetBufferSize - firstBlockSize);
        }
        if (hmac) {
            // Ensure the receive buffer is large enough to also hold the mac without expanding.
            this.receiveWriter.skip(hmac.digestLength);
        }
        const receiveBuffer = this.receiveWriter.toBuffer();
        const packetBuffer = receiveBuffer.slice(0, packetBufferSize);
        const macBuffer = receiveBuffer.slice(packetBufferSize);
        let followingBlocks = packetBuffer.slice(firstBlockSize, packetBufferSize);
        if (followingBlocks.length > 0) {
            if (!(await this.read(followingBlocks, cancellation))) {
                return null;
            }
            if (hmac === null || hmac === void 0 ? void 0 : hmac.encryptThenMac) {
                // In EtM mode, read and verify the MAC before decrypting.
                ///const packetWithoutLength = packet.slice(SshProtocol.packetLengthSize);
                if (!(await this.readAndVerifyHmac(hmac, packetBuffer, macBuffer, cancellation))) {
                    return null;
                }
            }
            if (encryption) {
                if (hmac === null || hmac === void 0 ? void 0 : hmac.authenticatedEncryption) {
                    // With a GCM cipher, the MAC is required for decryption.
                    if (!(await this.read(macBuffer, cancellation))) {
                        return null;
                    }
                    // This doesn't actually verify anything yet (hence the return value is not checked);
                    // it sets the tag to be used for verification in the following transform call.
                    await hmac.verify(followingBlocks, macBuffer);
                }
                try {
                    followingBlocks = await encryption.transform(followingBlocks);
                }
                catch (e) {
                    if (hmac === null || hmac === void 0 ? void 0 : hmac.authenticatedEncryption) {
                        // GCM decryption failed to verify data + tag.
                        throw new errors_1.SshConnectionError('Invalid MAC', transportMessages_1.SshDisconnectReason.macError);
                    }
                    else {
                        throw e;
                    }
                }
                this.receiveWriter.position = firstBlockSize;
                this.receiveWriter.write(followingBlocks);
            }
        }
        if (hmac && !hmac.encryptThenMac && !hmac.authenticatedEncryption) {
            if (!(await this.readAndVerifyHmac(hmac, packetBuffer, macBuffer, cancellation))) {
                return null;
            }
        }
        const paddingLength = packetBuffer[SshProtocol.packetLengthSize];
        let payload = packetBuffer.slice(SshProtocol.packetLengthSize + SshProtocol.paddingLengthSize, SshProtocol.packetLengthSize + (packetLength - paddingLength));
        if (compression) {
            payload = compression.decompress(payload);
        }
        if (this.incomingMessagesHaveReconnectInfo) {
            // Read the extension info from the end of the payload.
            let lastSequenceSeenByRemote;
            let remoteTimeSinceLastReceived;
            if (this.incomingMessagesHaveLatencyInfo) {
                const reader = new sshData_1.SshDataReader(payload.slice(payload.length - 12, payload.length));
                lastSequenceSeenByRemote = reader.readUInt64();
                remoteTimeSinceLastReceived = reader.readUInt32() / 1000; // microseconds to ms
                payload = payload.slice(0, payload.length - 12);
            }
            else {
                const reader = new sshData_1.SshDataReader(payload.slice(payload.length - 8, payload.length));
                lastSequenceSeenByRemote = reader.readUInt64();
                remoteTimeSinceLastReceived = 0;
                payload = payload.slice(0, payload.length - 8);
            }
            // Discard any recently sent messages that were acknowledged.
            while (this.recentSentMessages.size > 0) {
                const oldestSequenceMessage = this.recentSentMessages.peek();
                if (oldestSequenceMessage.sequence > lastSequenceSeenByRemote) {
                    break;
                }
                if (this.stream &&
                    this.incomingMessagesHaveLatencyInfo &&
                    oldestSequenceMessage.sequence === lastSequenceSeenByRemote) {
                    // Compute the time since the message with the last-seen sequence was sent.
                    // Subtract the time between when the remote side received the message with the
                    // last-seen sequence and sent the current message.
                    const timeSinceSent = this.lastIncomingTimestamp - oldestSequenceMessage.sentTime;
                    const roundTripLatency = timeSinceSent - remoteTimeSinceLastReceived;
                    this.metrics.updateLatency(roundTripLatency, this.trace);
                }
                this.recentSentMessages.dequeue();
            }
        }
        const messageType = payload[0];
        let message = sshMessage_1.SshMessage.create(this.config, messageType, this.messageContext, payload);
        if (!message) {
            const unimplementedMessage = new transportMessages_1.UnimplementedMessage();
            unimplementedMessage.sequenceNumber = this.inboundPacketSequence;
            unimplementedMessage.unimplementedMessageType = messageType;
            message = unimplementedMessage;
        }
        if (!(message instanceof connectionMessages_1.ChannelDataMessage)) {
            this.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.receivingMessage, `Receiving #${this.inboundPacketSequence} ${message}`);
        }
        else if (this.traceChannelData) {
            this.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.receivingChannelData, `Receiving #${this.inboundPacketSequence} ${message}`);
        }
        await this.sessionSemaphore.wait(cancellation);
        this.inboundPacketSequence++;
        this.inboundFlow += packetLength;
        this.sessionSemaphore.release();
        this.metrics.addMessageReceived(SshProtocol.packetLengthSize + packetLength + ((_a = hmac === null || hmac === void 0 ? void 0 : hmac.digestLength) !== null && _a !== void 0 ? _a : 0));
        await this.considerReExchange(false, cancellation);
        return message;
    }
    dispose() {
        try {
            if (this.stream)
                this.stream.close().catch((e) => {
                    this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamCloseError, `Error closing stream: ${e.message}`, e);
                });
        }
        catch (e) {
            if (!(e instanceof Error))
                throw e;
            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamCloseError, `Error closing stream: ${e.message}`, e);
        }
        this.stream = null;
        this.metrics.updateLatency(0);
        if (this.algorithms)
            this.algorithms.dispose();
    }
}
exports.SshProtocol = SshProtocol;
SshProtocol.maxPacketLength = 1024 * 1024; // 1 MB
SshProtocol.packetLengthSize = 4;
SshProtocol.paddingLengthSize = 1;
//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/6a44c352bd24569c417e530095901b649960f9f8/node_modules/@microsoft/dev-tunnels-ssh/io/sshProtocol.js.map