"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.TunnelRelayTunnelHost = void 0;
const dev_tunnels_contracts_1 = require("@microsoft/dev-tunnels-contracts");
const dev_tunnels_ssh_1 = require("@microsoft/dev-tunnels-ssh");
const dev_tunnels_ssh_tcp_1 = require("@microsoft/dev-tunnels-ssh-tcp");
const sshHelpers_1 = require("./sshHelpers");
const multiModeTunnelHost_1 = require("./multiModeTunnelHost");
const sessionPortKey_1 = require("./sessionPortKey");
const portRelayConnectRequestMessage_1 = require("./messages/portRelayConnectRequestMessage");
const portRelayConnectResponseMessage_1 = require("./messages/portRelayConnectResponseMessage");
const uuid_1 = require("uuid");
const sshHelpers_2 = require("./sshHelpers");
const tunnelConnectionSession_1 = require("./tunnelConnectionSession");
const webSocketSubProtocol = 'tunnel-relay-host';
const webSocketSubProtocolv2 = 'tunnel-relay-host-v2-dev';
// Check for an environment variable to determine which protocol version to use.
// By default, prefer V2 and fall back to V1.
const protocolVersion = (process === null || process === void 0 ? void 0 : process.env) && process.env.DEVTUNNELS_PROTOCOL_VERSION;
const connectionProtocols = protocolVersion === '1' ? [webSocketSubProtocol] :
    protocolVersion === '2' ? [webSocketSubProtocolv2] :
        [webSocketSubProtocolv2, webSocketSubProtocol];
/**
 * Tunnel host implementation that uses data-plane relay
 *  to accept client connections.
 */
class TunnelRelayTunnelHost extends tunnelConnectionSession_1.TunnelConnectionSession {
    constructor(managementClient, trace) {
        super(dev_tunnels_contracts_1.TunnelAccessScopes.Host, connectionProtocols, managementClient, trace);
        this.clientSessionPromises = [];
        this.reconnectableSessions = [];
        /**
         * Sessions created between this host and clients
         * @internal
         */
        this.sshSessions = [];
        /**
         * Port Forwarders between host and clients
         */
        this.remoteForwarders = new Map();
        this.loopbackIp = '127.0.0.1';
        this.forwardConnectionsToLocalPortsValue = (0, sshHelpers_2.isNode)();
        const publicKey = dev_tunnels_ssh_1.SshAlgorithms.publicKey.ecdsaSha2Nistp384;
        if (publicKey) {
            this.hostPrivateKeyPromise = publicKey.generateKeyPair();
        }
        this.hostId = multiModeTunnelHost_1.MultiModeTunnelHost.hostId;
        this.id = (0, uuid_1.v4)() + "-relay";
    }
    get connectionId() {
        return this.hostId;
    }
    /**
     * A value indicating whether the port-forwarding service forwards connections to local TCP sockets.
     * Forwarded connections are not possible if the host is not NodeJS (e.g. browser).
     * The default value for NodeJS hosts is true.
     */
    get forwardConnectionsToLocalPorts() {
        return this.forwardConnectionsToLocalPortsValue;
    }
    set forwardConnectionsToLocalPorts(value) {
        if (value === this.forwardConnectionsToLocalPortsValue) {
            return;
        }
        if (value && !(0, sshHelpers_2.isNode)()) {
            throw new Error('Cannot forward connections to local TCP sockets on this platform.');
        }
        this.forwardConnectionsToLocalPortsValue = value;
    }
    /**
     * Connects to a tunnel as a host and starts accepting incoming connections
     * to local ports as defined on the tunnel.
     * @deprecated Use `connect()` instead.
     */
    async start(tunnel) {
        await this.connect(tunnel);
    }
    /**
     * Connects to a tunnel as a host and starts accepting incoming connections
     * to local ports as defined on the tunnel.
     */
    async connect(tunnel, options, cancellation) {
        await this.connectTunnelSession(tunnel, options, cancellation);
    }
    /**
     * Connect to the tunnel session with the tunnel connector.
     * @param tunnel Tunnel to use for the connection.
     *     Undefined if the connection information is already known and the tunnel is not needed.
     *     Tunnel object to get the connection information from that tunnel.
     */
    async connectTunnelSession(tunnel, options, cancellation) {
        if (this.disconnectReason === dev_tunnels_ssh_1.SshDisconnectReason.tooManyConnections) {
            // If another host for the same tunnel connects, the first connection is disconnected
            // with "too many connections" reason. Reconnecting it again would cause the second host to
            // be kicked out, and then it would try to reconnect, kicking out this one.
            // To prevent this tug of war, do not allow reconnection in this case.
            throw new dev_tunnels_ssh_1.SshConnectionError('Cannot retry connection because another host for this tunnel has connected. ' +
                'Only one host connection at a time is supported.', dev_tunnels_ssh_1.SshDisconnectReason.tooManyConnections);
        }
        await super.connectTunnelSession(tunnel, options, cancellation);
    }
    /**
     * Configures the tunnel session with the given stream.
     * @internal
     */
    async configureSession(stream, protocol, isReconnect, cancellation) {
        this.connectionProtocol = protocol;
        let session;
        if (this.connectionProtocol === webSocketSubProtocol) {
            // The V1 protocol always configures no security, equivalent to SSH MultiChannelStream.
            // The websocket transport is still encrypted and authenticated.
            session = new dev_tunnels_ssh_1.SshClientSession(new dev_tunnels_ssh_1.SshSessionConfiguration(false)); // no encryption
        }
        else {
            session = sshHelpers_1.SshHelpers.createSshClientSession((config) => {
                // The V2 protocol configures optional encryption, including "none" as an enabled
                // and preferred key-exchange algorithm, because encryption of the outer SSH
                // session is optional since it is already over a TLS websocket.
                config.keyExchangeAlgorithms.splice(0, 0, dev_tunnels_ssh_1.SshAlgorithms.keyExchange.none);
                config.addService(dev_tunnels_ssh_tcp_1.PortForwardingService);
            });
            const hostPfs = session.activateService(dev_tunnels_ssh_tcp_1.PortForwardingService);
            hostPfs.messageFactory = this;
            hostPfs.onForwardedPortConnecting(this.onForwardedPortConnecting, this, this.sshSessionDisposables);
        }
        session.onChannelOpening(this.hostSession_ChannelOpening, this, this.sshSessionDisposables);
        session.onClosed(this.onSshSessionClosed, this, this.sshSessionDisposables);
        session.trace = this.trace;
        session.onReportProgress((args) => this.raiseReportProgress(args.progress, args.sessionNumber), this, this.sshSessionDisposables);
        this.sshSession = session;
        await session.connect(stream, cancellation);
        // SSH authentication is skipped in V1 protocol, optional in V2 depending on whether the
        // session performed a key exchange (as indicated by having a session ID or not). In the
        // latter case a password is not required. Strong authentication was already handled by
        // the relay service via the tunnel access token used for the websocket connection.
        if (session.sessionId) {
            await session.authenticate({ username: 'tunnel' });
        }
        if (this.connectionProtocol === webSocketSubProtocolv2) {
            // In the v2 protocol, the host starts "forwarding" the ports as soon as it connects.
            // Then the relay will forward the forwarded ports to clients as they connect.
            await this.startForwardingExistingPorts(session);
        }
    }
    /**
     * Validate the {@link tunnel} and get data needed to connect to it, if the tunnel is provided;
     * otherwise, ensure that there is already sufficient data to connect to a tunnel.
     * @internal
     */
    async onConnectingToTunnel() {
        var _a, _b, _c, _d, _e;
        if (!this.hostPrivateKey || !this.hostPublicKeys) {
            if (!this.hostPrivateKeyPromise) {
                throw new Error('Cannot create host keys');
            }
            this.hostPrivateKey = await this.hostPrivateKeyPromise;
            const buffer = await this.hostPrivateKey.getPublicKeyBytes(this.hostPrivateKey.keyAlgorithmName);
            if (!buffer) {
                throw new Error('Host private key public key bytes is not initialized');
            }
            this.hostPublicKeys = [buffer.toString('base64')];
        }
        const tunnelHasSshPort = ((_a = this.tunnel) === null || _a === void 0 ? void 0 : _a.ports) != null && this.tunnel.ports.find((v) => v.protocol === dev_tunnels_contracts_1.TunnelProtocol.Ssh);
        const endpointSignature = `${(_b = this.tunnel) === null || _b === void 0 ? void 0 : _b.tunnelId}.${(_c = this.tunnel) === null || _c === void 0 ? void 0 : _c.clusterId}:` +
            `${(_d = this.tunnel) === null || _d === void 0 ? void 0 : _d.name}.${(_e = this.tunnel) === null || _e === void 0 ? void 0 : _e.domain}:` +
            `${tunnelHasSshPort}:${this.hostId}:${this.hostPublicKeys}`;
        if (!this.relayUri || this.endpointSignature !== endpointSignature) {
            if (!this.tunnel) {
                throw new Error('Tunnel is required');
            }
            let endpoint = {
                id: this.id,
                hostId: this.hostId,
                hostPublicKeys: this.hostPublicKeys,
                connectionMode: dev_tunnels_contracts_1.TunnelConnectionMode.TunnelRelay,
            };
            let additionalQueryParameters = undefined;
            if (tunnelHasSshPort) {
                additionalQueryParameters = { includeSshGatewayPublicKey: 'true' };
            }
            endpoint = await this.managementClient.updateTunnelEndpoint(this.tunnel, endpoint, {
                additionalQueryParameters: additionalQueryParameters,
            });
            this.relayUri = endpoint.hostRelayUri;
            this.endpointSignature = endpointSignature;
        }
    }
    /**
     * Disposes this tunnel session, closing all client connections, the host SSH session, and deleting the endpoint.
     */
    async dispose() {
        await super.dispose();
        const promises = Object.assign([], this.clientSessionPromises);
        // No new client session should be added because the channel requests are rejected when the tunnel host is disposed.
        this.clientSessionPromises.length = 0;
        // If the tunnel is present, the endpoint was created, and this host was not closed because of
        // too many connections, delete the endpoint.
        // Too many connections closure means another host has connected, and that other host, while
        // connecting, would have updated the endpoint. So this host won't be able to delete it anyway.
        if (this.tunnel &&
            this.endpointSignature &&
            this.disconnectReason !== dev_tunnels_ssh_1.SshDisconnectReason.tooManyConnections) {
            const promise = this.managementClient.deleteTunnelEndpoints(this.tunnel, this.id);
            promises.push(promise);
        }
        for (const forwarder of this.remoteForwarders.values()) {
            forwarder.dispose();
        }
        // When client session promises finish, they remove the sessions from this.sshSessions
        await Promise.all(promises);
    }
    hostSession_ChannelOpening(e) {
        if (!e.isRemoteRequest) {
            // Auto approve all local requests (not that there are any for the time being).
            return;
        }
        if (this.connectionProtocol === webSocketSubProtocolv2 &&
            e.channel.channelType === 'forwarded-tcpip') {
            // With V2 protocol, the relay server always sends an extended channel open message
            // with a property indicating whether E2E encryption is requested for the connection.
            // The host returns an extended response message indicating if E2EE is enabled.
            const relayRequestMessage = e.channel.openMessage
                .convertTo(new portRelayConnectRequestMessage_1.PortRelayConnectRequestMessage());
            const responseMessage = new portRelayConnectResponseMessage_1.PortRelayConnectResponseMessage();
            // The host can enable encryption for the channel if the client requested it.
            responseMessage.isE2EEncryptionEnabled = this.enableE2EEncryption &&
                relayRequestMessage.isE2EEncryptionRequested;
            // In the future the relay might send additional information in the connect
            // request message, for example a user identifier that would enable the host to
            // group channels by user.
            e.openingPromise = Promise.resolve(responseMessage);
            return;
        }
        else if (e.channel.channelType !== TunnelRelayTunnelHost.clientStreamChannelType) {
            e.failureDescription = `Unknown channel type: ${e.channel.channelType}`;
            e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.unknownChannelType;
            return;
        }
        // V1 protocol.
        // Increase max window size to work around channel congestion bug.
        // This does not entirely eliminate the problem, but reduces the chance.
        e.channel.maxWindowSize = dev_tunnels_ssh_1.SshChannel.defaultMaxWindowSize * 5;
        if (this.isDisposed) {
            e.failureDescription = 'The host is disconnecting.';
            e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.connectFailed;
            return;
        }
        const promise = this.acceptClientSession(e.channel, this.disposeToken);
        this.clientSessionPromises.push(promise);
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        promise.then(() => {
            const index = this.clientSessionPromises.indexOf(promise);
            this.clientSessionPromises.splice(index, 1);
        });
    }
    onForwardedPortConnecting(e) {
        const channel = e.stream.channel;
        const relayRequestMessage = channel.openMessage.convertTo(new portRelayConnectRequestMessage_1.PortRelayConnectRequestMessage());
        const isE2EEncryptionEnabled = this.enableE2EEncryption &&
            relayRequestMessage.isE2EEncryptionRequested;
        if (isE2EEncryptionEnabled) {
            // Increase the max window size so that it is at least larger than the window
            // size of one client channel.
            channel.maxWindowSize = dev_tunnels_ssh_1.SshChannel.defaultMaxWindowSize * 2;
            const serverCredentials = {
                publicKeys: [this.hostPrivateKey]
            };
            const secureStream = new dev_tunnels_ssh_1.SecureStream(e.stream, serverCredentials, this.reconnectableSessions);
            secureStream.trace = this.trace;
            // The client was already authenticated by the relay.
            secureStream.onAuthenticating((authEvent) => authEvent.authenticationPromise = Promise.resolve({}));
            // The client will connect to the secure stream after the channel is opened.
            secureStream.connect().catch((err) => {
                this.trace(dev_tunnels_ssh_1.TraceLevel.Error, 0, `Error connecting encrypted channel: ${err}`);
            });
            e.transformPromise = Promise.resolve(secureStream);
        }
        super.onForwardedPortConnecting(e);
    }
    async acceptClientSession(clientSessionChannel, cancellation) {
        try {
            const stream = new dev_tunnels_ssh_1.SshStream(clientSessionChannel);
            await this.connectAndRunClientSession(stream, cancellation);
        }
        catch (ex) {
            if (!(ex instanceof dev_tunnels_ssh_1.CancellationError) || !cancellation.isCancellationRequested) {
                this.trace(dev_tunnels_ssh_1.TraceLevel.Error, 0, `Error running client SSH session: ${ex}`);
            }
        }
    }
    /**
     * Creates an SSH server session for a client (V1 protocol), runs the session,
     * and waits for it to close.
     */
    async connectAndRunClientSession(stream, cancellation) {
        if (cancellation.isCancellationRequested) {
            stream.destroy();
            throw new dev_tunnels_ssh_1.CancellationError();
        }
        const clientChannelId = stream.channel.channelId;
        const session = sshHelpers_1.SshHelpers.createSshServerSession(this.reconnectableSessions, (config) => {
            var _a;
            config.protocolExtensions.push(dev_tunnels_ssh_1.SshProtocolExtensionNames.sessionReconnect);
            config.addService(dev_tunnels_ssh_tcp_1.PortForwardingService);
            // Configure keep-alive if requested
            const keepAliveInterval = (_a = this.connectionOptions) === null || _a === void 0 ? void 0 : _a.keepAliveIntervalInSeconds;
            if (keepAliveInterval && keepAliveInterval > 0) {
                config.keepAliveTimeoutInSeconds = keepAliveInterval;
            }
        });
        session.trace = this.trace;
        session.onReportProgress((args) => this.raiseReportProgress(args.progress, args.sessionNumber), this, this.sshSessionDisposables);
        session.credentials = {
            publicKeys: [this.hostPrivateKey],
        };
        const tcs = new dev_tunnels_ssh_1.PromiseCompletionSource();
        const authenticatingEventRegistration = session.onAuthenticating((e) => {
            this.onSshClientAuthenticating(e);
        });
        session.onClientAuthenticated(() => {
            // This call is async and will catch and log any async errors.
            void this.onSshClientAuthenticated(session);
        });
        const requestRegistration = session.onRequest((e) => {
            this.onClientSessionRequest(e, session);
        });
        const channelOpeningEventRegistration = session.onChannelOpening((e) => {
            this.onSshChannelOpening(e, session);
        });
        const reconnectedEventRegistration = session.onReconnected(() => {
            this.onClientSessionReconnecting(session, clientChannelId);
        });
        const closedEventRegistration = session.onClosed((e) => {
            this.onClientSessionClosed(session, e, clientChannelId, cancellation);
            tcs.resolve();
        });
        session.onKeepAliveFailed((count) => this.onKeepAliveFailed(count));
        session.onKeepAliveSucceeded((count) => this.onKeepAliveSucceeded(count));
        try {
            const nodeStream = new dev_tunnels_ssh_1.NodeStream(stream);
            await session.connect(nodeStream);
            this.sshSessions.push(session);
            cancellation.onCancellationRequested((e) => {
                tcs.reject(new dev_tunnels_ssh_1.CancellationError());
            });
            if (this.tunnel && this.managementClient) {
                const connectedEvent = {
                    name: 'host_client_connect',
                    properties: {
                        ClientChannelId: clientChannelId.toString(),
                        ClientSessionId: this.getShortSessionId(session),
                        HostSessionId: this.connectionId,
                    }
                };
                this.managementClient.reportEvent(this.tunnel, connectedEvent);
            }
            await tcs.promise;
        }
        finally {
            authenticatingEventRegistration.dispose();
            requestRegistration.dispose();
            channelOpeningEventRegistration.dispose();
            reconnectedEventRegistration.dispose();
            closedEventRegistration.dispose();
            await session.close(dev_tunnels_ssh_1.SshDisconnectReason.byApplication);
            session.dispose();
        }
    }
    onSshClientAuthenticating(e) {
        if (e.authenticationType === dev_tunnels_ssh_1.SshAuthenticationType.clientNone) {
            // For now, the client is allowed to skip SSH authentication;
            // they must have a valid tunnel access token already to get this far.
            e.authenticationPromise = Promise.resolve({});
        }
        else {
            // Other authentication types are not implemented. Doing nothing here
            // results in a client authentication failure.
        }
    }
    async onSshClientAuthenticated(session) {
        void this.startForwardingExistingPorts(session);
    }
    async startForwardingExistingPorts(session) {
        var _a, _b;
        const pfs = session.activateService(dev_tunnels_ssh_tcp_1.PortForwardingService);
        pfs.forwardConnectionsToLocalPorts = this.forwardConnectionsToLocalPorts;
        // Ports must be forwarded sequentially because the TS SSH lib
        // does not yet support concurrent requests.
        for (const port of (_b = (_a = this.tunnel) === null || _a === void 0 ? void 0 : _a.ports) !== null && _b !== void 0 ? _b : []) {
            this.trace(dev_tunnels_ssh_1.TraceLevel.Verbose, 0, `Forwarding port ${port.portNumber}`);
            try {
                await this.forwardPort(pfs, port);
            }
            catch (ex) {
                this.traceError(`Error forwarding port ${port.portNumber}: ${ex}`);
            }
        }
    }
    onClientSessionRequest(e, session) {
        if (e.requestType === 'RefreshPorts') {
            e.responsePromise = (async () => {
                await this.refreshPorts();
                return new dev_tunnels_ssh_1.SessionRequestSuccessMessage();
            })();
        }
    }
    onSshChannelOpening(e, session) {
        if (!(e.request instanceof dev_tunnels_ssh_tcp_1.PortForwardChannelOpenMessage)) {
            // This is to let the Go SDK open an unused session channel
            if (e.request.channelType === dev_tunnels_ssh_1.SshChannel.sessionChannelType) {
                return;
            }
            this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, 0, 'Rejecting request to open non-portforwarding channel.');
            e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.administrativelyProhibited;
            return;
        }
        const portForwardRequest = e.request;
        if (portForwardRequest.channelType === 'direct-tcpip') {
            if (!this.tunnel.ports.some((p) => p.portNumber === portForwardRequest.port)) {
                this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, 0, 'Rejecting request to connect to non-forwarded port:' + portForwardRequest.port);
                e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.administrativelyProhibited;
            }
        }
        else if (portForwardRequest.channelType === 'forwarded-tcpip') {
            const eventArgs = new dev_tunnels_ssh_tcp_1.ForwardedPortConnectingEventArgs(portForwardRequest.port, false, new dev_tunnels_ssh_1.SshStream(e.channel));
            super.onForwardedPortConnecting(eventArgs);
        }
        else {
            // For forwarded-tcpip do not check remoteForwarders because they may not be updated yet.
            // There is a small time interval in forwardPort() between the port
            // being forwarded with forwardFromRemotePort and remoteForwarders updated.
            // Setting PFS.acceptRemoteConnectionsForNonForwardedPorts to false makes PFS reject forwarding requests from the
            // clients for the ports that are not forwarded and are missing in PFS.remoteConnectors.
            // Call to pfs.forwardFromRemotePort() in forwardPort() adds the connector to PFS.remoteConnectors.
            this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, 0, 'Nonrecognized channel type ' + portForwardRequest.channelType);
            e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.unknownChannelType;
        }
    }
    onClientSessionReconnecting(session, clientChannelId) {
        if (this.tunnel && this.managementClient) {
            const reconnectedEvent = {
                name: 'host_client_reconnect',
                properties: {
                    ClientChannelId: clientChannelId.toString(),
                    ClientSessionId: this.getShortSessionId(session),
                    HostSessionId: this.connectionId,
                }
            };
            this.managementClient.reportEvent(this.tunnel, reconnectedEvent);
        }
    }
    onClientSessionClosed(session, e, clientChannelId, cancellation) {
        // Determine severity based on the disconnect reason
        let severity;
        let details;
        // Reconnecting client session may cause the new session to close with 'None' reason.
        if (e.reason === dev_tunnels_ssh_1.SshDisconnectReason.byApplication) {
            details = 'Client ssh session closed by application.';
            this.traceInfo(details);
        }
        else if (cancellation.isCancellationRequested) {
            details = 'Client ssh session cancelled.';
            this.traceInfo(details);
        }
        else if (e.reason !== dev_tunnels_ssh_1.SshDisconnectReason.none) {
            severity = dev_tunnels_contracts_1.TunnelEvent.error;
            details = `Client ssh session closed unexpectedly due to ${e.reason}, ` +
                `"${e.message}"\n${e.error}`;
            this.traceError(details);
        }
        else {
            details = 'Client ssh session closed.';
        }
        // Report client disconnected event
        if (this.tunnel && this.managementClient) {
            const disconnectedEvent = {
                timestamp: new Date(),
                name: 'host_client_disconnect',
                severity: severity,
                details: details,
                properties: {
                    ClientChannelId: clientChannelId.toString(),
                    ClientSessionId: this.getShortSessionId(session),
                    HostSessionId: this.connectionId,
                }
            };
            this.managementClient.reportEvent(this.tunnel, disconnectedEvent);
        }
        for (const [key, forwarder] of this.remoteForwarders.entries()) {
            if (forwarder.session === session) {
                forwarder.dispose();
                this.remoteForwarders.delete(key);
            }
        }
        const index = this.sshSessions.indexOf(session);
        if (index >= 0) {
            this.sshSessions.splice(index, 1);
        }
    }
    async refreshPorts(cancellation) {
        var _a, _b;
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingRefreshPorts);
        if (!await this.refreshTunnel(true, cancellation)) {
            return;
        }
        const ports = (_b = (_a = this.tunnel) === null || _a === void 0 ? void 0 : _a.ports) !== null && _b !== void 0 ? _b : [];
        let sessions = this.sshSessions;
        if (this.connectionProtocol === webSocketSubProtocolv2 && this.sshSession) {
            // In the V2 protocol, ports are forwarded directly on the host session.
            // (But even when the host is V2, some clients may still connect with V1.)
            sessions = [...sessions, this.sshSession];
        }
        const forwardPromises = [];
        for (const port of ports) {
            // For all sessions which are connected and authenticated, forward any added/updated
            // ports. For sessions that are not yet authenticated, the ports will be forwarded
            // immediately after authentication completes - see onSshClientAuthenticated().
            // (Session requests may not be sent before the session is authenticated, for sessions
            // that require authentication; For V2 sessions that are not encrypted/authenticated
            // at all, the session ID is null.)
            for (const session of sessions.filter((s) => s.isConnected && (!s.sessionId || s.principal))) {
                const key = new sessionPortKey_1.SessionPortKey(session.sessionId, Number(port.portNumber));
                const forwarder = this.remoteForwarders.get(key.toString());
                if (!forwarder) {
                    const pfs = session.getService(dev_tunnels_ssh_tcp_1.PortForwardingService);
                    forwardPromises.push(this.forwardPort(pfs, port));
                }
            }
        }
        for (const [key, forwarder] of Object.entries(this.remoteForwarders)) {
            if (!ports.some((p) => p.portNumber === forwarder.localPort)) {
                this.remoteForwarders.delete(key);
                forwarder.dispose();
            }
        }
        await Promise.all(forwardPromises);
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedRefreshPorts);
    }
    async forwardPort(pfs, port) {
        const portNumber = Number(port.portNumber);
        if (pfs.localForwardedPorts.find((p) => p.localPort === portNumber)) {
            // The port is already forwarded. This may happen if we try to add the same port twice after reconnection.
            return;
        }
        // When forwarding from a Remote port we assume that the RemotePortNumber
        // and requested LocalPortNumber are the same.
        const forwarder = await pfs.forwardFromRemotePort(this.loopbackIp, portNumber, 'localhost', portNumber);
        if (!forwarder) {
            // The forwarding request was rejected by the client.
            return;
        }
        const key = new sessionPortKey_1.SessionPortKey(pfs.session.sessionId, Number(forwarder.localPort));
        this.remoteForwarders.set(key.toString(), forwarder);
    }
}
exports.TunnelRelayTunnelHost = TunnelRelayTunnelHost;
TunnelRelayTunnelHost.webSocketSubProtocol = webSocketSubProtocol;
TunnelRelayTunnelHost.webSocketSubProtocolv2 = webSocketSubProtocolv2;
/**
 * Ssh channel type in host relay ssh session where client session streams are passed.
 */
TunnelRelayTunnelHost.clientStreamChannelType = 'client-ssh-session-stream';
//# sourceMappingURL=https://github.com/VSCodium/sourcemaps/releases/download/stable-4c0b0c6cc561d2d3636d1ec250935431876ce4dc/node_modules-@microsoft-dev-tunnels-connections-tunnelRelayTunnelHost.js.map