Initial connection handling
This commit is contained in:
parent
310bfe509e
commit
4861405683
6 changed files with 363 additions and 85 deletions
|
@ -52,6 +52,11 @@ How to [secure your setup](/doc/security/ssl.md).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
- Clone this as `vs/src/server` in the VS Code source.
|
||||||
|
- Run `yarn watch-client`in the VS Code root.
|
||||||
|
- Run `node out/vs/server/main.js`.
|
||||||
|
- Visit `http://localhost:8443`.
|
||||||
|
|
||||||
### Known Issues
|
### Known Issues
|
||||||
|
|
||||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
- Creating custom VS Code extensions and debugging them doesn't work.
|
||||||
|
|
28
connection.ts
Normal file
28
connection.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Emitter } from "vs/base/common/event";
|
||||||
|
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
||||||
|
import { VSBuffer } from "vs/base/common/buffer";
|
||||||
|
|
||||||
|
export abstract class Connection {
|
||||||
|
protected readonly _onClose = new Emitter<void>();
|
||||||
|
public readonly onClose = this._onClose.event;
|
||||||
|
|
||||||
|
public constructor(private readonly protocol: PersistentProtocol) {
|
||||||
|
this.protocol.onSocketClose(() => {
|
||||||
|
// TODO: eventually we'll want to clean up the connection if nothing
|
||||||
|
// ever connects back to it
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public reconnect(socket: ISocket, buffer: VSBuffer): void {
|
||||||
|
this.protocol.beginAcceptReconnection(socket, buffer);
|
||||||
|
this.protocol.endAcceptReconnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManagementConnection extends Connection {
|
||||||
|
// in here they accept the connection
|
||||||
|
// to the ipc of the RemoteServer
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExtensionHostConnection extends Connection {
|
||||||
|
}
|
1
main.js
Normal file
1
main.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
require("../../bootstrap-amd").load("vs/server/server");
|
164
server.ts
Normal file
164
server.ts
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as http from "http";
|
||||||
|
import * as net from "net";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as util from "util";
|
||||||
|
import * as url from "url";
|
||||||
|
|
||||||
|
import { Connection } from "vs/server/connection";
|
||||||
|
import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection";
|
||||||
|
import { Emitter } from "vs/base/common/event";
|
||||||
|
import { ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
|
||||||
|
import { Socket, Server as IServer } from "vs/server/socket";
|
||||||
|
|
||||||
|
enum HttpCode {
|
||||||
|
Ok = 200,
|
||||||
|
NotFound = 404,
|
||||||
|
BadRequest = 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpError extends Error {
|
||||||
|
public constructor(message: string, public readonly code: number) {
|
||||||
|
super(message);
|
||||||
|
// @ts-ignore
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Server implements IServer {
|
||||||
|
private readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
|
||||||
|
public readonly onDidClientConnect = this._onDidClientConnect.event;
|
||||||
|
|
||||||
|
private readonly rootPath = path.resolve(__dirname, "../../..");
|
||||||
|
|
||||||
|
private readonly server: http.Server;
|
||||||
|
|
||||||
|
public readonly connections = new Map<ConnectionType, Map<string, Connection>>();
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.server = http.createServer(async (request, response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const content = await this.handleRequest(request);
|
||||||
|
response.writeHead(HttpCode.Ok, {
|
||||||
|
"Cache-Control": "max-age=86400",
|
||||||
|
// TODO: ETag?
|
||||||
|
});
|
||||||
|
response.end(content);
|
||||||
|
} catch (error) {
|
||||||
|
response.writeHead(typeof error.code === "number" ? error.code : 500);
|
||||||
|
response.end(error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on("upgrade", (request, socket) => {
|
||||||
|
this.handleUpgrade(request, socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on("error", (error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRequest(request: http.IncomingMessage): Promise<string | Buffer> {
|
||||||
|
if (request.method !== "GET") {
|
||||||
|
throw new HttpError(
|
||||||
|
`Unsupported method ${request.method}`,
|
||||||
|
HttpCode.BadRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPath = url.parse(request.url || "").pathname || "/";
|
||||||
|
if (requestPath === "/") {
|
||||||
|
const htmlPath = path.join(
|
||||||
|
this.rootPath,
|
||||||
|
'out/vs/code/browser/workbench/workbench.html',
|
||||||
|
);
|
||||||
|
|
||||||
|
let html = await util.promisify(fs.readFile)(htmlPath, "utf8");
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
WEBVIEW_ENDPOINT: {},
|
||||||
|
WORKBENCH_WEB_CONGIGURATION: {
|
||||||
|
remoteAuthority: request.headers.host,
|
||||||
|
},
|
||||||
|
REMOTE_USER_DATA_URI: {
|
||||||
|
scheme: "http",
|
||||||
|
authority: request.headers.host,
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
PRODUCT_CONFIGURATION: {},
|
||||||
|
CONNECTION_AUTH_TOKEN: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(options).forEach((key) => {
|
||||||
|
html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
html = html.replace('{{WEBVIEW_ENDPOINT}}', JSON.stringify(options.WEBVIEW_ENDPOINT));
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await util.promisify(fs.readFile)(
|
||||||
|
path.join(this.rootPath, requestPath),
|
||||||
|
);
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||||
|
throw new HttpError("Not found", HttpCode.NotFound);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleUpgrade(request: http.IncomingMessage, socket: net.Socket): void {
|
||||||
|
if (request.headers.upgrade !== "websocket") {
|
||||||
|
return socket.end("HTTP/1.1 400 Bad Request");
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
reconnectionToken: "",
|
||||||
|
reconnection: false,
|
||||||
|
skipWebSocketFrames: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.url) {
|
||||||
|
const query = url.parse(request.url, true).query;
|
||||||
|
if (query.reconnectionToken) {
|
||||||
|
options.reconnectionToken = query.reconnectionToken as string;
|
||||||
|
}
|
||||||
|
if (query.reconnection === "true") {
|
||||||
|
options.reconnection = true;
|
||||||
|
}
|
||||||
|
if (query.skipWebSocketFrames === "true") {
|
||||||
|
options.skipWebSocketFrames = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeSocket = new Socket(socket, options);
|
||||||
|
nodeSocket.upgrade(request.headers["sec-websocket-key"] as string);
|
||||||
|
nodeSocket.handshake(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public listen(): void {
|
||||||
|
const port = 8443;
|
||||||
|
this.server.listen(port, () => {
|
||||||
|
const address = this.server.address();
|
||||||
|
const location = typeof address === "string"
|
||||||
|
? address
|
||||||
|
: `port ${address.port}`;
|
||||||
|
console.log(`Listening on ${location}`);
|
||||||
|
console.log(`Serving ${this.rootPath}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new Server();
|
||||||
|
server.listen();
|
161
socket.ts
Normal file
161
socket.ts
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as net from "net";
|
||||||
|
import { AuthRequest, ConnectionType, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection";
|
||||||
|
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
|
||||||
|
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
||||||
|
import { VSBuffer } from "vs/base/common/buffer";
|
||||||
|
import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/connection";
|
||||||
|
|
||||||
|
export interface SocketOptions {
|
||||||
|
readonly reconnectionToken: string;
|
||||||
|
readonly reconnection: boolean;
|
||||||
|
readonly skipWebSocketFrames: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Server {
|
||||||
|
readonly connections: Map<ConnectionType, Map<string, Connection>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Socket {
|
||||||
|
private nodeSocket: ISocket;
|
||||||
|
public protocol: PersistentProtocol;
|
||||||
|
|
||||||
|
public constructor(private readonly socket: net.Socket, private readonly options: SocketOptions) {
|
||||||
|
socket.on("error", () => this.dispose());
|
||||||
|
this.nodeSocket = new NodeSocket(socket);
|
||||||
|
if (!this.options.skipWebSocketFrames) {
|
||||||
|
this.nodeSocket = new WebSocketNodeSocket(this.nodeSocket as NodeSocket);
|
||||||
|
}
|
||||||
|
this.protocol = new PersistentProtocol(this.nodeSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade the connection into a web socket.
|
||||||
|
*/
|
||||||
|
public upgrade(secWebsocketKey: string): void {
|
||||||
|
// This magic value is specified by the websocket spec.
|
||||||
|
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
const reply = crypto.createHash("sha1")
|
||||||
|
.update(secWebsocketKey + magic)
|
||||||
|
.digest("base64");
|
||||||
|
|
||||||
|
this.socket.write([
|
||||||
|
"HTTP/1.1 101 Switching Protocols",
|
||||||
|
"Upgrade: websocket",
|
||||||
|
"Connection: Upgrade",
|
||||||
|
`Sec-WebSocket-Accept: ${reply}`,
|
||||||
|
].join("\r\n") + "\r\n\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.nodeSocket.dispose();
|
||||||
|
this.protocol.dispose();
|
||||||
|
this.nodeSocket = undefined!;
|
||||||
|
this.protocol = undefined!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public handshake(server: Server): void {
|
||||||
|
const handler = this.protocol.onControlMessage((rawMessage) => {
|
||||||
|
const message = JSON.parse(rawMessage.toString());
|
||||||
|
switch (message.type) {
|
||||||
|
case "auth": return this.authenticate(message);
|
||||||
|
case "connectionType":
|
||||||
|
handler.dispose();
|
||||||
|
return this.connect(message, server);
|
||||||
|
case "default":
|
||||||
|
return this.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: This ignores the authentication process entirely for now.
|
||||||
|
*/
|
||||||
|
private authenticate(_message: AuthRequest): void {
|
||||||
|
this.sendControl({
|
||||||
|
type: "sign",
|
||||||
|
data: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private connect(message: ConnectionTypeRequest, server: Server): void {
|
||||||
|
switch (message.desiredConnectionType) {
|
||||||
|
case ConnectionType.ExtensionHost:
|
||||||
|
case ConnectionType.Management:
|
||||||
|
const debugPort = this.getDebugPort();
|
||||||
|
const ok = message.desiredConnectionType === ConnectionType.ExtensionHost
|
||||||
|
? (debugPort ? { debugPort } : {})
|
||||||
|
: { type: "ok" };
|
||||||
|
|
||||||
|
if (!server.connections.has(message.desiredConnectionType)) {
|
||||||
|
server.connections.set(message.desiredConnectionType, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = server.connections.get(message.desiredConnectionType)!;
|
||||||
|
|
||||||
|
if (this.options.reconnection && connections.has(this.options.reconnectionToken)) {
|
||||||
|
this.sendControl(ok);
|
||||||
|
const buffer = this.protocol.readEntireBuffer();
|
||||||
|
this.protocol.dispose();
|
||||||
|
return connections.get(this.options.reconnectionToken)!
|
||||||
|
.reconnect(this.nodeSocket, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.reconnection || connections.has(this.options.reconnectionToken)) {
|
||||||
|
this.sendControl({
|
||||||
|
type: "error",
|
||||||
|
reason: this.options.reconnection
|
||||||
|
? "Unrecognized reconnection token"
|
||||||
|
: "Duplicate reconnection token",
|
||||||
|
});
|
||||||
|
return this.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendControl(ok);
|
||||||
|
|
||||||
|
const connection = message.desiredConnectionType === ConnectionType.Management
|
||||||
|
? new ManagementConnection(this.protocol)
|
||||||
|
: new ExtensionHostConnection(this.protocol);
|
||||||
|
|
||||||
|
connections.set(this.options.reconnectionToken, connection);
|
||||||
|
connection.onClose(() => {
|
||||||
|
connections.delete(this.options.reconnectionToken);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ConnectionType.Tunnel:
|
||||||
|
return this.tunnel();
|
||||||
|
default:
|
||||||
|
this.sendControl({
|
||||||
|
type: "error",
|
||||||
|
reason: "Unrecognized connection type",
|
||||||
|
});
|
||||||
|
return this.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: implement.
|
||||||
|
*/
|
||||||
|
private tunnel(): void {
|
||||||
|
this.sendControl({
|
||||||
|
type: "error",
|
||||||
|
reason: "Tunnel is not implemented yet",
|
||||||
|
});
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: implement.
|
||||||
|
*/
|
||||||
|
private getDebugPort(): number | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a handshake message. In the case of the extension host, it just sends
|
||||||
|
* back a debug port.
|
||||||
|
*/
|
||||||
|
private sendControl(message: HandshakeMessage | { debugPort?: number } ): void {
|
||||||
|
this.protocol.sendControl(VSBuffer.fromString(JSON.stringify(message)));
|
||||||
|
}
|
||||||
|
}
|
89
tslint.json
89
tslint.json
|
@ -1,89 +1,8 @@
|
||||||
{
|
{
|
||||||
"rulesDirectory": "./rules/dist",
|
"extends": [
|
||||||
|
"../../../tslint.json"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"only-arrow-functions": true,
|
"no-unexternalized-strings": false
|
||||||
"curly-statement-newlines": true,
|
|
||||||
"no-block-padding": true,
|
|
||||||
"adjacent-overload-signatures": true,
|
|
||||||
"align": true,
|
|
||||||
"await-promise": [true, "Thenable"],
|
|
||||||
"class-name": true,
|
|
||||||
"eofline": true,
|
|
||||||
"import-spacing": true,
|
|
||||||
"indent": [true, "tabs"],
|
|
||||||
"no-angle-bracket-type-assertion": false,
|
|
||||||
"no-bitwise": false,
|
|
||||||
"no-any": true,
|
|
||||||
"newline-before-return": true,
|
|
||||||
"no-console": true,
|
|
||||||
"no-duplicate-imports": true,
|
|
||||||
"no-consecutive-blank-lines": true,
|
|
||||||
"no-empty": true,
|
|
||||||
"no-floating-promises": true,
|
|
||||||
"no-return-await": true,
|
|
||||||
"no-var-keyword": true,
|
|
||||||
"no-trailing-whitespace": true,
|
|
||||||
"no-redundant-jsdoc": true,
|
|
||||||
"no-implicit-dependencies": false,
|
|
||||||
"no-boolean-literal-compare": true,
|
|
||||||
"prefer-readonly": true,
|
|
||||||
"deprecation": true,
|
|
||||||
"semicolon": true,
|
|
||||||
"one-line": [
|
|
||||||
true,
|
|
||||||
"check-catch",
|
|
||||||
"check-finally",
|
|
||||||
"check-else",
|
|
||||||
"check-whitespace",
|
|
||||||
"check-open-brace"
|
|
||||||
],
|
|
||||||
"completed-docs": {
|
|
||||||
"options": [
|
|
||||||
true,
|
|
||||||
"enums",
|
|
||||||
"functions",
|
|
||||||
"methods",
|
|
||||||
"classes"
|
|
||||||
],
|
|
||||||
"severity": "warning"
|
|
||||||
},
|
|
||||||
"no-unused-expression": [
|
|
||||||
true,
|
|
||||||
"allow-fast-null-checks"
|
|
||||||
],
|
|
||||||
"curly": [
|
|
||||||
true
|
|
||||||
],
|
|
||||||
"quotemark": [
|
|
||||||
true,
|
|
||||||
"double",
|
|
||||||
"avoid-escape",
|
|
||||||
"avoid-template"
|
|
||||||
],
|
|
||||||
"trailing-comma": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"multiline": "always",
|
|
||||||
"singleline": "never",
|
|
||||||
"esSpecCompliant": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"space-before-function-paren": [
|
|
||||||
false,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"member-access": [
|
|
||||||
true,
|
|
||||||
"check-accessor",
|
|
||||||
"check-constructor",
|
|
||||||
"check-parameter-property"
|
|
||||||
],
|
|
||||||
"typedef": [
|
|
||||||
true,
|
|
||||||
"call-signature",
|
|
||||||
"arrow-call-signature",
|
|
||||||
"parameter",
|
|
||||||
"property-declaration"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue