mirror of
https://github.com/coder/code-server.git
synced 2026-05-26 06:07:26 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
44
lib/vscode/src/vs/platform/remote/node/nodeSocketFactory.ts
Normal file
44
lib/vscode/src/vs/platform/remote/node/nodeSocketFactory.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { ISocketFactory, IConnectCallback } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
|
||||
export const nodeSocketFactory = new class implements ISocketFactory {
|
||||
connect(host: string, port: number, query: string, callback: IConnectCallback): void {
|
||||
const errorListener = (err: any) => callback(err, undefined);
|
||||
|
||||
const socket = net.createConnection({ host: host, port: port }, () => {
|
||||
socket.removeListener('error', errorListener);
|
||||
|
||||
// https://tools.ietf.org/html/rfc6455#section-4
|
||||
const buffer = Buffer.alloc(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
buffer[i] = Math.round(Math.random() * 256);
|
||||
}
|
||||
const nonce = buffer.toString('base64');
|
||||
|
||||
let headers = [
|
||||
`GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`,
|
||||
`Connection: Upgrade`,
|
||||
`Upgrade: websocket`,
|
||||
`Sec-WebSocket-Key: ${nonce}`
|
||||
];
|
||||
socket.write(headers.join('\r\n') + '\r\n\r\n');
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
const strData = data.toString();
|
||||
if (strData.indexOf('\r\n\r\n') >= 0) {
|
||||
// headers received OK
|
||||
socket.off('data', onData);
|
||||
callback(undefined, new NodeSocket(socket));
|
||||
}
|
||||
};
|
||||
socket.on('data', onData);
|
||||
});
|
||||
socket.once('error', errorListener);
|
||||
}
|
||||
};
|
||||
169
lib/vscode/src/vs/platform/remote/node/tunnelService.ts
Normal file
169
lib/vscode/src/vs/platform/remote/node/tunnelService.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { findFreePortFaster } from 'vs/base/node/ports';
|
||||
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
|
||||
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
|
||||
async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
|
||||
const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
|
||||
return tunnel.waitForReady();
|
||||
}
|
||||
|
||||
class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
|
||||
|
||||
public readonly tunnelRemotePort: number;
|
||||
public tunnelLocalPort!: number;
|
||||
public tunnelRemoteHost: string;
|
||||
public localAddress!: string;
|
||||
|
||||
private readonly _options: IConnectionOptions;
|
||||
private readonly _server: net.Server;
|
||||
private readonly _barrier: Barrier;
|
||||
|
||||
private readonly _listeningListener: () => void;
|
||||
private readonly _connectionListener: (socket: net.Socket) => void;
|
||||
private readonly _errorListener: () => void;
|
||||
|
||||
private readonly _socketsDispose: Map<string, () => void> = new Map();
|
||||
|
||||
constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
|
||||
super();
|
||||
this._options = options;
|
||||
this._server = net.createServer();
|
||||
this._barrier = new Barrier();
|
||||
|
||||
this._listeningListener = () => this._barrier.open();
|
||||
this._server.on('listening', this._listeningListener);
|
||||
|
||||
this._connectionListener = (socket) => this._onConnection(socket);
|
||||
this._server.on('connection', this._connectionListener);
|
||||
|
||||
// If there is no error listener and there is an error it will crash the whole window
|
||||
this._errorListener = () => { };
|
||||
this._server.on('error', this._errorListener);
|
||||
|
||||
this.tunnelRemotePort = tunnelRemotePort;
|
||||
this.tunnelRemoteHost = tunnelRemoteHost;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._server.removeListener('listening', this._listeningListener);
|
||||
this._server.removeListener('connection', this._connectionListener);
|
||||
this._server.removeListener('error', this._errorListener);
|
||||
this._server.close();
|
||||
const disposers = Array.from(this._socketsDispose.values());
|
||||
disposers.forEach(disposer => {
|
||||
disposer();
|
||||
});
|
||||
}
|
||||
|
||||
public async waitForReady(): Promise<this> {
|
||||
// try to get the same port number as the remote port number...
|
||||
let localPort = await findFreePortFaster(this.suggestedLocalPort ?? this.tunnelRemotePort, 2, 1000);
|
||||
|
||||
// if that fails, the method above returns 0, which works out fine below...
|
||||
let address: string | net.AddressInfo | null = null;
|
||||
address = (<net.AddressInfo>this._server.listen(localPort).address());
|
||||
|
||||
// It is possible for findFreePortFaster to return a port that there is already a server listening on. This causes the previous listen call to error out.
|
||||
if (!address) {
|
||||
localPort = 0;
|
||||
address = (<net.AddressInfo>this._server.listen(localPort).address());
|
||||
}
|
||||
|
||||
this.tunnelLocalPort = address.port;
|
||||
|
||||
await this._barrier.wait();
|
||||
this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
|
||||
return this;
|
||||
}
|
||||
|
||||
private async _onConnection(localSocket: net.Socket): Promise<void> {
|
||||
// pause reading on the socket until we have a chance to forward its data
|
||||
localSocket.pause();
|
||||
|
||||
const protocol = await connectRemoteAgentTunnel(this._options, this.tunnelRemotePort);
|
||||
const remoteSocket = (<NodeSocket>protocol.getSocket()).socket;
|
||||
const dataChunk = protocol.readEntireBuffer();
|
||||
protocol.dispose();
|
||||
|
||||
if (dataChunk.byteLength > 0) {
|
||||
localSocket.write(dataChunk.buffer);
|
||||
}
|
||||
|
||||
localSocket.on('end', () => {
|
||||
this._socketsDispose.delete(localSocket.localAddress);
|
||||
remoteSocket.end();
|
||||
});
|
||||
localSocket.on('close', () => remoteSocket.end());
|
||||
localSocket.on('error', () => {
|
||||
this._socketsDispose.delete(localSocket.localAddress);
|
||||
remoteSocket.destroy();
|
||||
});
|
||||
|
||||
remoteSocket.on('end', () => localSocket.end());
|
||||
remoteSocket.on('close', () => localSocket.end());
|
||||
remoteSocket.on('error', () => {
|
||||
localSocket.destroy();
|
||||
});
|
||||
|
||||
localSocket.pipe(remoteSocket);
|
||||
remoteSocket.pipe(localSocket);
|
||||
this._socketsDispose.set(localSocket.localAddress, () => {
|
||||
// Need to end instead of unpipe, otherwise whatever is connected locally could end up "stuck" with whatever state it had until manually exited.
|
||||
localSocket.end();
|
||||
remoteSocket.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TunnelService extends AbstractTunnelService {
|
||||
public constructor(
|
||||
@ILogService logService: ILogService,
|
||||
@ISignService private readonly signService: ISignService,
|
||||
@IProductService private readonly productService: IProductService
|
||||
) {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
|
||||
const existing = this.getTunnelFromMap(remoteHost, remotePort);
|
||||
if (existing) {
|
||||
++existing.refcount;
|
||||
return existing.value;
|
||||
}
|
||||
|
||||
if (this._tunnelProvider) {
|
||||
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort });
|
||||
if (tunnel) {
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
}
|
||||
return tunnel;
|
||||
} else {
|
||||
const options: IConnectionOptions = {
|
||||
commit: this.productService.commit,
|
||||
socketFactory: nodeSocketFactory,
|
||||
addressProvider,
|
||||
signService: this.signService,
|
||||
logService: this.logService,
|
||||
ipcLogger: null
|
||||
};
|
||||
|
||||
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
return tunnel;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user