From 1a033819ad45994f7a4bff294a59a3e5f0619738 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Fri, 22 Mar 2024 14:44:57 +0100 Subject: [PATCH] Support remote port forwarding (#13439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * basics for dev-container support Signed-off-by: Jonah Iden * basic creating and connecting to container working Signed-off-by: Jonah Iden * open workspace when opening container Signed-off-by: Jonah Iden * save and reuse last USed container per workspace Signed-off-by: Jonah Iden * restart container if running Signed-off-by: Jonah Iden * better container creation extension features Signed-off-by: Jonah Iden * added dockerfile support Signed-off-by: Jonah Iden * rebuild container if devcontainer.json has been changed since last use Signed-off-by: Jonah Iden * fix build Signed-off-by: Jonah Iden * fixed checking if container needs rebuild Signed-off-by: Jonah Iden * working port forwarding via exec instance Signed-off-by: Jonah Iden * review changes Signed-off-by: Jonah Iden * fix import Signed-off-by: Jonah Iden * smaller fixes and added support for multiple devcontainer configuration files Signed-off-by: Jonah Iden * basic output window for devcontainer build Signed-off-by: Jonah Iden * smaller review changes and nicer dockerfile.json detection code Signed-off-by: Jonah Iden * fixed build and docuemented implemented devcontainer.json properties Signed-off-by: Jonah Iden * Fix unneeded URI conversion (#13415) * Fix quickpick problems found in IDE testing (#13451) Fixes #13450, #13449 contributed on behalf of STMicroelectronics Signed-off-by: Thomas Mäder * Fix rending of quickpick buttons (#13342) Ensure that the Theia specific wrapper for the MonacoQuickPickItem properly forwards assignments of the "buttons" property to the wrapped item. Fixes #13076 Contributed on behalf of STMicroelectronics * electron: allow accessing the metrics endpoint for performance analysis (#13380) By default, when running Theia in Electron, all endpoints are protected by the ElectronTokenValidator. This patch allows accessing the '/metrics' endpoint without a token, which enables us to collect metrics for performance analysis. For this, ElectronTokenValidator is extended to allow access to the metrics endpoint. All other endpoints are still protected. Contributed on behalf of STMicroelectronics Signed-off-by: Olaf Lessenich * fixed renaming and moving of open notebooks (#13467) * fixed renameing of open notebooks Signed-off-by: Jonah Iden * fixed moving of notebook editors to other areas Signed-off-by: Jonah Iden --------- Signed-off-by: Jonah Iden * [playwright] Update documentation Since a recent enhancement/refactoring of @theia/playwright, to permit using it in Theia Electron applications, the way to load an application has changed. This commit is an attempt to update the examples that are part of the documentation. I validated the changes in the "theia-playwright-template" repository, and so I have adapted the sample code to that repo's linting rules (using single quotes instead of double). It's possible that other things have changed, that I have not yet encountered, but this should be a good step forward, at least for those just getting started integrating playwright to test their Theia-based app. Signed-off-by: Marc Dumais * basics for dev-container support Signed-off-by: Jonah Iden * basic creating and connecting to container working Signed-off-by: Jonah Iden * added dockerfile support Signed-off-by: Jonah Iden * added port forwarding inlcuding ui Signed-off-by: Jonah Iden * basic port/address validation Signed-off-by: Jonah Iden * fixed allready forwarded port checking Signed-off-by: Jonah Iden * rebase fixes Signed-off-by: Jonah Iden * removed unused file Signed-off-by: Jonah Iden * review changes Signed-off-by: Jonah Iden * fixed widget focus and message margin Signed-off-by: Jonah Iden * default port binding now shows as 0.0.0.0 Signed-off-by: Jonah Iden --------- Signed-off-by: Jonah Iden Signed-off-by: Thomas Mäder Signed-off-by: Olaf Lessenich Signed-off-by: Marc Dumais Co-authored-by: Alexander Taran Co-authored-by: Thomas Mäder Co-authored-by: Tobias Ortmayr Co-authored-by: Olaf Lessenich Co-authored-by: Marc Dumais --- .../remote-container-connection-provider.ts | 11 +- .../port-forwading-contribution.ts | 33 +++++ .../port-forwarding-service.ts | 84 +++++++++++ .../port-forwarding-widget.tsx | 140 ++++++++++++++++++ .../remote-frontend-module.ts | 25 +++- .../style/port-forwarding-widget.css | 44 ++++++ .../remote-port-forwarding-provider.ts | 29 ++++ .../electron-node/remote-backend-module.ts | 6 + .../remote-port-forwarding-provider.ts | 50 +++++++ .../remote/src/electron-node/remote-types.ts | 2 +- .../ssh/remote-ssh-connection-provider.ts | 4 +- 11 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts create mode 100644 packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts create mode 100644 packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx create mode 100644 packages/remote/src/electron-browser/style/port-forwarding-widget.css create mode 100644 packages/remote/src/electron-common/remote-port-forwarding-provider.ts create mode 100644 packages/remote/src/electron-node/remote-port-forwarding-provider.ts diff --git a/packages/dev-container/src/electron-node/remote-container-connection-provider.ts b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts index 6c80b89e57c7b..214b98b3a19ad 100644 --- a/packages/dev-container/src/electron-node/remote-container-connection-provider.ts +++ b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts @@ -149,6 +149,13 @@ interface ContainerTerminalSession { executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>; } +interface ContainerTerminalSession { + execution: Docker.Exec, + stdout: WriteStream, + stderr: WriteStream, + executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>; +} + export class RemoteDockerContainerConnection implements RemoteConnection { id: string; @@ -179,12 +186,12 @@ export class RemoteDockerContainerConnection implements RemoteConnection { this.container = options.container; } - async forwardOut(socket: Socket): Promise { + async forwardOut(socket: Socket, port?: number): Promise { const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`; const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`; try { const ttySession = await this.container.exec({ - Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${this.remotePort}`], + Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${port ?? this.remotePort}`], AttachStdin: true, AttachStdout: true, AttachStderr: true }); diff --git a/packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts b/packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts new file mode 100644 index 0000000000000..0f45e5465abf1 --- /dev/null +++ b/packages/remote/src/electron-browser/port-forwarding/port-forwading-contribution.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { nls } from '@theia/core'; +import { AbstractViewContribution } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; +import { PortForwardingWidget, PORT_FORWARDING_WIDGET_ID } from './port-forwarding-widget'; + +@injectable() +export class PortForwardingContribution extends AbstractViewContribution { + constructor() { + super({ + widgetId: PORT_FORWARDING_WIDGET_ID, + widgetName: nls.localizeByDefault('Ports'), + defaultWidgetOptions: { + area: 'bottom' + } + }); + } +} diff --git a/packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts new file mode 100644 index 0000000000000..109cfb098a30d --- /dev/null +++ b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-service.ts @@ -0,0 +1,84 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Emitter } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemotePortForwardingProvider } from '../../electron-common/remote-port-forwarding-provider'; + +export interface ForwardedPort { + localPort?: number; + address?: string; + origin?: string; + editing: boolean; +} + +@injectable() +export class PortForwardingService { + + @inject(RemotePortForwardingProvider) + readonly provider: RemotePortForwardingProvider; + + protected readonly onDidChangePortsEmitter = new Emitter(); + readonly onDidChangePorts = this.onDidChangePortsEmitter.event; + + forwardedPorts: ForwardedPort[] = []; + + forwardNewPort(origin?: string): ForwardedPort { + const index = this.forwardedPorts.push({ editing: true, origin }); + return this.forwardedPorts[index - 1]; + } + + updatePort(port: ForwardedPort, newAdress: string): void { + const connectionPort = new URLSearchParams(location.search).get('port'); + if (!connectionPort) { + // if there is no open remote connection we can't forward a port + return; + } + + const parts = newAdress.split(':'); + if (parts.length === 2) { + port.address = parts[0]; + port.localPort = parseInt(parts[1]); + } else { + port.localPort = parseInt(parts[0]); + } + + port.editing = false; + + this.provider.forwardPort(parseInt(connectionPort), { port: port.localPort!, address: port.address }); + this.onDidChangePortsEmitter.fire(); + } + + removePort(port: ForwardedPort): void { + const index = this.forwardedPorts.indexOf(port); + if (index !== -1) { + this.forwardedPorts.splice(index, 1); + this.provider.portRemoved({ port: port.localPort! }); + this.onDidChangePortsEmitter.fire(); + } + } + + isValidAddress(address: string): boolean { + const match = address.match(/^(.*:)?\d+$/); + if (!match) { + return false; + } + + const port = parseInt(address.includes(':') ? address.split(':')[1] : address); + + return !this.forwardedPorts.some(p => p.localPort === port); + } +} diff --git a/packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx new file mode 100644 index 0000000000000..9333e43f628cc --- /dev/null +++ b/packages/remote/src/electron-browser/port-forwarding/port-forwarding-widget.tsx @@ -0,0 +1,140 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as React from '@theia/core/shared/react'; +import { ReactNode } from '@theia/core/shared/react'; +import { OpenerService, ReactWidget } from '@theia/core/lib/browser'; +import { nls, URI } from '@theia/core'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { ForwardedPort, PortForwardingService } from './port-forwarding-service'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; + +export const PORT_FORWARDING_WIDGET_ID = 'port-forwarding-widget'; + +@injectable() +export class PortForwardingWidget extends ReactWidget { + + @inject(PortForwardingService) + protected readonly portForwardingService: PortForwardingService; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(ClipboardService) + protected readonly clipboardService: ClipboardService; + + @postConstruct() + protected init(): void { + this.id = PORT_FORWARDING_WIDGET_ID; + this.node.tabIndex = -1; + this.title.label = nls.localizeByDefault('Ports'); + this.title.caption = this.title.label; + this.title.closable = true; + this.update(); + + this.portForwardingService.onDidChangePorts(() => this.update()); + } + + protected render(): ReactNode { + if (this.portForwardingService.forwardedPorts.length === 0) { + return
+

+ {nls.localizeByDefault('No forwarded ports. Forward a port to access your locally running services over the internet.\n[Forward a Port]({0})').split('\n')[0]} +

+ {this.renderForwardPortButton()} +
; + } + + return
+ + + + + + + + + + + {this.portForwardingService.forwardedPorts.map(port => ( + + {this.renderPortColumn(port)} + {this.renderAddressColumn(port)} + + + + ))} + {!this.portForwardingService.forwardedPorts.some(port => port.editing) && } + +
{nls.localizeByDefault('Port')}{nls.localizeByDefault('Address')}{nls.localizeByDefault('Running Process')}{nls.localizeByDefault('Origin')}
{port.origin ? nls.localizeByDefault(port.origin) : ''}
{this.renderForwardPortButton()}
+
; + } + + protected renderForwardPortButton(): ReactNode { + return ; + } + + protected renderAddressColumn(port: ForwardedPort): ReactNode { + const address = `${port.address ?? '0.0.0.0'}:${port.localPort}`; + return +
+ { + if (e.ctrlKey) { + const uri = new URI(`http://${address}`); + (await this.openerService.getOpener(uri)).open(uri); + } + }} title={nls.localizeByDefault('Follow link') + ' (ctrl/cmd + click)'}> + {port.localPort ? address : ''} + + { + port.localPort && + { + this.clipboardService.writeText(address); + }}> + } +
+ ; + } + + protected renderPortColumn(port: ForwardedPort): ReactNode { + return port.editing ? + : + +
+ {port.localPort} + { + this.portForwardingService.removePort(port); + this.update(); + }}> +
+ ; + } + +} + +function PortEditingInput({ port, service }: { port: ForwardedPort, service: PortForwardingService }): React.JSX.Element { + const [error, setError] = React.useState(false); + return e.key === 'Enter' && !error && service.updatePort(port, e.currentTarget.value)} + onKeyUp={e => setError(!service.isValidAddress(e.currentTarget.value))}>; + +} diff --git a/packages/remote/src/electron-browser/remote-frontend-module.ts b/packages/remote/src/electron-browser/remote-frontend-module.ts index f2ddaf3d9e2d4..599c9ef3906d3 100644 --- a/packages/remote/src/electron-browser/remote-frontend-module.ts +++ b/packages/remote/src/electron-browser/remote-frontend-module.ts @@ -16,7 +16,7 @@ import { bindContributionProvider, CommandContribution } from '@theia/core'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser'; +import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser'; import { RemoteSSHContribution } from './remote-ssh-contribution'; import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderPath } from '../electron-common/remote-ssh-connection-provider'; import { RemoteFrontendContribution } from './remote-frontend-contribution'; @@ -26,6 +26,12 @@ import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common import { ElectronFileDialogService } from '@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-service'; import { RemoteElectronFileDialogService } from './remote-electron-file-dialog-service'; import { bindRemotePreferences } from './remote-preferences'; +import { PortForwardingWidget, PORT_FORWARDING_WIDGET_ID } from './port-forwarding/port-forwarding-widget'; +import { PortForwardingContribution } from './port-forwarding/port-forwading-contribution'; +import { PortForwardingService } from './port-forwarding/port-forwarding-service'; +import { RemotePortForwardingProvider, RemoteRemotePortForwardingProviderPath } from '../electron-common/remote-port-forwarding-provider'; +import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import '../../src/electron-browser/style/port-forwarding-widget.css'; export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteFrontendContribution).toSelf().inSingletonScope(); @@ -42,8 +48,21 @@ export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteService).toSelf().inSingletonScope(); + bind(PortForwardingWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: PORT_FORWARDING_WIDGET_ID, + createWidget: () => context.container.get(PortForwardingWidget) + })); + + bindViewContribution(bind, PortForwardingContribution); + bind(PortForwardingService).toSelf().inSingletonScope(); + bind(RemoteSSHConnectionProvider).toDynamicValue(ctx => - WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope(); + ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope(); bind(RemoteStatusService).toDynamicValue(ctx => - WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteStatusServicePath)).inSingletonScope(); + ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteStatusServicePath)).inSingletonScope(); + + bind(RemotePortForwardingProvider).toDynamicValue(ctx => + ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteRemotePortForwardingProviderPath)).inSingletonScope(); + }); diff --git a/packages/remote/src/electron-browser/style/port-forwarding-widget.css b/packages/remote/src/electron-browser/style/port-forwarding-widget.css new file mode 100644 index 0000000000000..7eafdb5e6af4e --- /dev/null +++ b/packages/remote/src/electron-browser/style/port-forwarding-widget.css @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (C) 2024 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 + ********************************************************************************/ + +.port-table { + width: 100%; + margin: calc(var(--theia-ui-padding) * 2); + table-layout: fixed; +} + +.port-table-header { + text-align: left; +} + +.forward-port-button { + margin-left: 0; + width: 100%; +} + +.button-cell { + display: flex; + padding-right: var(--theia-ui-padding); +} + +.forwarded-address:hover { + cursor: pointer; + text-decoration: underline; +} + +.port-edit-input-error { + outline-color: var(--theia-inputValidation-errorBorder); +} diff --git a/packages/remote/src/electron-common/remote-port-forwarding-provider.ts b/packages/remote/src/electron-common/remote-port-forwarding-provider.ts new file mode 100644 index 0000000000000..6f01e01da2ccd --- /dev/null +++ b/packages/remote/src/electron-common/remote-port-forwarding-provider.ts @@ -0,0 +1,29 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +export const RemoteRemotePortForwardingProviderPath = '/remote/port-forwarding'; + +export const RemotePortForwardingProvider = Symbol('RemoteSSHConnectionProvider'); + +export interface ForwardedPort { + port: number; + address?: string; +} + +export interface RemotePortForwardingProvider { + forwardPort(connectionPort: number, portToForward: ForwardedPort): Promise; + portRemoved(port: ForwardedPort): Promise; +} diff --git a/packages/remote/src/electron-node/remote-backend-module.ts b/packages/remote/src/electron-node/remote-backend-module.ts index 733929479ce44..2197bb28a3d8b 100644 --- a/packages/remote/src/electron-node/remote-backend-module.ts +++ b/packages/remote/src/electron-node/remote-backend-module.ts @@ -37,11 +37,17 @@ import { RemoteCopyContribution, RemoteCopyRegistry } from './setup/remote-copy- import { MainCopyContribution } from './setup/main-copy-contribution'; import { RemoteNativeDependencyContribution } from './setup/remote-native-dependency-contribution'; import { AppNativeDependencyContribution } from './setup/app-native-dependency-contribution'; +import { RemotePortForwardingProviderImpl } from './remote-port-forwarding-provider'; +import { RemotePortForwardingProvider, RemoteRemotePortForwardingProviderPath } from '../electron-common/remote-port-forwarding-provider'; export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(RemoteSSHConnectionProviderImpl).toSelf().inSingletonScope(); bind(RemoteSSHConnectionProvider).toService(RemoteSSHConnectionProviderImpl); bindBackendService(RemoteSSHConnectionProviderPath, RemoteSSHConnectionProvider); + + bind(RemotePortForwardingProviderImpl).toSelf().inSingletonScope(); + bind(RemotePortForwardingProvider).toService(RemotePortForwardingProviderImpl); + bindBackendService(RemoteRemotePortForwardingProviderPath, RemotePortForwardingProvider); }); export default new ContainerModule((bind, _unbind, _isBound, rebind) => { diff --git a/packages/remote/src/electron-node/remote-port-forwarding-provider.ts b/packages/remote/src/electron-node/remote-port-forwarding-provider.ts new file mode 100644 index 0000000000000..23c8e4ec9f579 --- /dev/null +++ b/packages/remote/src/electron-node/remote-port-forwarding-provider.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ForwardedPort, RemotePortForwardingProvider } from '../electron-common/remote-port-forwarding-provider'; +import { createServer, Server } from 'net'; +import { RemoteConnectionService } from './remote-connection-service'; + +@injectable() +export class RemotePortForwardingProviderImpl implements RemotePortForwardingProvider { + + @inject(RemoteConnectionService) + protected readonly connectionService: RemoteConnectionService; + + protected forwardedPorts: Map = new Map(); + + async forwardPort(connectionPort: number, portToForward: ForwardedPort): Promise { + const currentConnection = this.connectionService.getConnectionFromPort(connectionPort); + if (!currentConnection) { + throw new Error(`No connection found for port ${connectionPort}`); + } + + const server = createServer(socket => { + currentConnection?.forwardOut(socket, portToForward.port); + }).listen(portToForward.port, portToForward.address); + this.forwardedPorts.set(portToForward.port, server); + } + + async portRemoved(forwardedPort: ForwardedPort): Promise { + const proxy = this.forwardedPorts.get(forwardedPort.port); + if (proxy) { + proxy.close(); + this.forwardedPorts.delete(forwardedPort.port); + } + } + +} diff --git a/packages/remote/src/electron-node/remote-types.ts b/packages/remote/src/electron-node/remote-types.ts index e50811629d0cc..9c829ee201499 100644 --- a/packages/remote/src/electron-node/remote-types.ts +++ b/packages/remote/src/electron-node/remote-types.ts @@ -49,7 +49,7 @@ export interface RemoteConnection extends Disposable { localPort: number; remotePort: number; onDidDisconnect: Event; - forwardOut(socket: net.Socket): void; + forwardOut(socket: net.Socket, port?: number): void; /** * execute a single command on the remote machine diff --git a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts index a0b20e321c235..7c9342c0dd8ff 100644 --- a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts +++ b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts @@ -278,8 +278,8 @@ export class RemoteSSHConnection implements RemoteConnection { return sftpClient; } - forwardOut(socket: net.Socket): void { - this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', this.remotePort, (err, stream) => { + forwardOut(socket: net.Socket, port?: number): void { + this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', port ?? this.remotePort, (err, stream) => { if (err) { console.debug('Proxy message rejected', err); } else {