Skip to content

Commit 4861405

Browse files
committed
Initial connection handling
1 parent 310bfe5 commit 4861405

File tree

6 files changed

+363
-85
lines changed

6 files changed

+363
-85
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ How to [secure your setup](/doc/security/ssl.md).
5252
5353
## Development
5454
55+
- Clone this as `vs/src/server` in the VS Code source.
56+
- Run `yarn watch-client`in the VS Code root.
57+
- Run `node out/vs/server/main.js`.
58+
- Visit `http://localhost:8443`.
59+
5560
### Known Issues
5661
5762
- Creating custom VS Code extensions and debugging them doesn't work.

connection.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Emitter } from "vs/base/common/event";
2+
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
3+
import { VSBuffer } from "vs/base/common/buffer";
4+
5+
export abstract class Connection {
6+
protected readonly _onClose = new Emitter<void>();
7+
public readonly onClose = this._onClose.event;
8+
9+
public constructor(private readonly protocol: PersistentProtocol) {
10+
this.protocol.onSocketClose(() => {
11+
// TODO: eventually we'll want to clean up the connection if nothing
12+
// ever connects back to it
13+
});
14+
}
15+
16+
public reconnect(socket: ISocket, buffer: VSBuffer): void {
17+
this.protocol.beginAcceptReconnection(socket, buffer);
18+
this.protocol.endAcceptReconnection();
19+
}
20+
}
21+
22+
export class ManagementConnection extends Connection {
23+
// in here they accept the connection
24+
// to the ipc of the RemoteServer
25+
}
26+
27+
export class ExtensionHostConnection extends Connection {
28+
}

main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require("../../bootstrap-amd").load("vs/server/server");

server.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import * as fs from "fs";
2+
import * as http from "http";
3+
import * as net from "net";
4+
import * as path from "path";
5+
import * as util from "util";
6+
import * as url from "url";
7+
8+
import { Connection } from "vs/server/connection";
9+
import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection";
10+
import { Emitter } from "vs/base/common/event";
11+
import { ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
12+
import { Socket, Server as IServer } from "vs/server/socket";
13+
14+
enum HttpCode {
15+
Ok = 200,
16+
NotFound = 404,
17+
BadRequest = 400,
18+
}
19+
20+
class HttpError extends Error {
21+
public constructor(message: string, public readonly code: number) {
22+
super(message);
23+
// @ts-ignore
24+
this.name = this.constructor.name;
25+
Error.captureStackTrace(this, this.constructor);
26+
}
27+
}
28+
29+
class Server implements IServer {
30+
private readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
31+
public readonly onDidClientConnect = this._onDidClientConnect.event;
32+
33+
private readonly rootPath = path.resolve(__dirname, "../../..");
34+
35+
private readonly server: http.Server;
36+
37+
public readonly connections = new Map<ConnectionType, Map<string, Connection>>();
38+
39+
public constructor() {
40+
this.server = http.createServer(async (request, response): Promise<void> => {
41+
try {
42+
const content = await this.handleRequest(request);
43+
response.writeHead(HttpCode.Ok, {
44+
"Cache-Control": "max-age=86400",
45+
// TODO: ETag?
46+
});
47+
response.end(content);
48+
} catch (error) {
49+
response.writeHead(typeof error.code === "number" ? error.code : 500);
50+
response.end(error.message);
51+
}
52+
});
53+
54+
this.server.on("upgrade", (request, socket) => {
55+
this.handleUpgrade(request, socket);
56+
});
57+
58+
this.server.on("error", (error) => {
59+
console.error(error);
60+
process.exit(1);
61+
});
62+
}
63+
64+
public dispose(): void {
65+
this.connections.clear();
66+
}
67+
68+
private async handleRequest(request: http.IncomingMessage): Promise<string | Buffer> {
69+
if (request.method !== "GET") {
70+
throw new HttpError(
71+
`Unsupported method ${request.method}`,
72+
HttpCode.BadRequest,
73+
);
74+
}
75+
76+
const requestPath = url.parse(request.url || "").pathname || "/";
77+
if (requestPath === "/") {
78+
const htmlPath = path.join(
79+
this.rootPath,
80+
'out/vs/code/browser/workbench/workbench.html',
81+
);
82+
83+
let html = await util.promisify(fs.readFile)(htmlPath, "utf8");
84+
85+
const options = {
86+
WEBVIEW_ENDPOINT: {},
87+
WORKBENCH_WEB_CONGIGURATION: {
88+
remoteAuthority: request.headers.host,
89+
},
90+
REMOTE_USER_DATA_URI: {
91+
scheme: "http",
92+
authority: request.headers.host,
93+
path: "/",
94+
},
95+
PRODUCT_CONFIGURATION: {},
96+
CONNECTION_AUTH_TOKEN: {}
97+
};
98+
99+
Object.keys(options).forEach((key) => {
100+
html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
101+
});
102+
103+
html = html.replace('{{WEBVIEW_ENDPOINT}}', JSON.stringify(options.WEBVIEW_ENDPOINT));
104+
105+
return html;
106+
}
107+
108+
try {
109+
const content = await util.promisify(fs.readFile)(
110+
path.join(this.rootPath, requestPath),
111+
);
112+
return content;
113+
} catch (error) {
114+
if (error.code === "ENOENT" || error.code === "EISDIR") {
115+
throw new HttpError("Not found", HttpCode.NotFound);
116+
}
117+
throw error;
118+
}
119+
}
120+
121+
private handleUpgrade(request: http.IncomingMessage, socket: net.Socket): void {
122+
if (request.headers.upgrade !== "websocket") {
123+
return socket.end("HTTP/1.1 400 Bad Request");
124+
}
125+
126+
const options = {
127+
reconnectionToken: "",
128+
reconnection: false,
129+
skipWebSocketFrames: false,
130+
};
131+
132+
if (request.url) {
133+
const query = url.parse(request.url, true).query;
134+
if (query.reconnectionToken) {
135+
options.reconnectionToken = query.reconnectionToken as string;
136+
}
137+
if (query.reconnection === "true") {
138+
options.reconnection = true;
139+
}
140+
if (query.skipWebSocketFrames === "true") {
141+
options.skipWebSocketFrames = true;
142+
}
143+
}
144+
145+
const nodeSocket = new Socket(socket, options);
146+
nodeSocket.upgrade(request.headers["sec-websocket-key"] as string);
147+
nodeSocket.handshake(this);
148+
}
149+
150+
public listen(): void {
151+
const port = 8443;
152+
this.server.listen(port, () => {
153+
const address = this.server.address();
154+
const location = typeof address === "string"
155+
? address
156+
: `port ${address.port}`;
157+
console.log(`Listening on ${location}`);
158+
console.log(`Serving ${this.rootPath}`);
159+
});
160+
}
161+
}
162+
163+
const server = new Server();
164+
server.listen();

socket.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import * as crypto from "crypto";
2+
import * as net from "net";
3+
import { AuthRequest, ConnectionType, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection";
4+
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
5+
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
6+
import { VSBuffer } from "vs/base/common/buffer";
7+
import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/connection";
8+
9+
export interface SocketOptions {
10+
readonly reconnectionToken: string;
11+
readonly reconnection: boolean;
12+
readonly skipWebSocketFrames: boolean;
13+
}
14+
15+
export interface Server {
16+
readonly connections: Map<ConnectionType, Map<string, Connection>>;
17+
}
18+
19+
export class Socket {
20+
private nodeSocket: ISocket;
21+
public protocol: PersistentProtocol;
22+
23+
public constructor(private readonly socket: net.Socket, private readonly options: SocketOptions) {
24+
socket.on("error", () => this.dispose());
25+
this.nodeSocket = new NodeSocket(socket);
26+
if (!this.options.skipWebSocketFrames) {
27+
this.nodeSocket = new WebSocketNodeSocket(this.nodeSocket as NodeSocket);
28+
}
29+
this.protocol = new PersistentProtocol(this.nodeSocket);
30+
}
31+
32+
/**
33+
* Upgrade the connection into a web socket.
34+
*/
35+
public upgrade(secWebsocketKey: string): void {
36+
// This magic value is specified by the websocket spec.
37+
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
38+
const reply = crypto.createHash("sha1")
39+
.update(secWebsocketKey + magic)
40+
.digest("base64");
41+
42+
this.socket.write([
43+
"HTTP/1.1 101 Switching Protocols",
44+
"Upgrade: websocket",
45+
"Connection: Upgrade",
46+
`Sec-WebSocket-Accept: ${reply}`,
47+
].join("\r\n") + "\r\n\r\n");
48+
}
49+
50+
public dispose(): void {
51+
this.nodeSocket.dispose();
52+
this.protocol.dispose();
53+
this.nodeSocket = undefined!;
54+
this.protocol = undefined!;
55+
}
56+
57+
public handshake(server: Server): void {
58+
const handler = this.protocol.onControlMessage((rawMessage) => {
59+
const message = JSON.parse(rawMessage.toString());
60+
switch (message.type) {
61+
case "auth": return this.authenticate(message);
62+
case "connectionType":
63+
handler.dispose();
64+
return this.connect(message, server);
65+
case "default":
66+
return this.dispose();
67+
}
68+
});
69+
}
70+
71+
/**
72+
* TODO: This ignores the authentication process entirely for now.
73+
*/
74+
private authenticate(_message: AuthRequest): void {
75+
this.sendControl({
76+
type: "sign",
77+
data: "",
78+
});
79+
}
80+
81+
private connect(message: ConnectionTypeRequest, server: Server): void {
82+
switch (message.desiredConnectionType) {
83+
case ConnectionType.ExtensionHost:
84+
case ConnectionType.Management:
85+
const debugPort = this.getDebugPort();
86+
const ok = message.desiredConnectionType === ConnectionType.ExtensionHost
87+
? (debugPort ? { debugPort } : {})
88+
: { type: "ok" };
89+
90+
if (!server.connections.has(message.desiredConnectionType)) {
91+
server.connections.set(message.desiredConnectionType, new Map());
92+
}
93+
94+
const connections = server.connections.get(message.desiredConnectionType)!;
95+
96+
if (this.options.reconnection && connections.has(this.options.reconnectionToken)) {
97+
this.sendControl(ok);
98+
const buffer = this.protocol.readEntireBuffer();
99+
this.protocol.dispose();
100+
return connections.get(this.options.reconnectionToken)!
101+
.reconnect(this.nodeSocket, buffer);
102+
}
103+
104+
if (this.options.reconnection || connections.has(this.options.reconnectionToken)) {
105+
this.sendControl({
106+
type: "error",
107+
reason: this.options.reconnection
108+
? "Unrecognized reconnection token"
109+
: "Duplicate reconnection token",
110+
});
111+
return this.dispose();
112+
}
113+
114+
this.sendControl(ok);
115+
116+
const connection = message.desiredConnectionType === ConnectionType.Management
117+
? new ManagementConnection(this.protocol)
118+
: new ExtensionHostConnection(this.protocol);
119+
120+
connections.set(this.options.reconnectionToken, connection);
121+
connection.onClose(() => {
122+
connections.delete(this.options.reconnectionToken);
123+
});
124+
break;
125+
case ConnectionType.Tunnel:
126+
return this.tunnel();
127+
default:
128+
this.sendControl({
129+
type: "error",
130+
reason: "Unrecognized connection type",
131+
});
132+
return this.dispose();
133+
}
134+
}
135+
136+
/**
137+
* TODO: implement.
138+
*/
139+
private tunnel(): void {
140+
this.sendControl({
141+
type: "error",
142+
reason: "Tunnel is not implemented yet",
143+
});
144+
this.dispose();
145+
}
146+
147+
/**
148+
* TODO: implement.
149+
*/
150+
private getDebugPort(): number | undefined {
151+
return undefined;
152+
}
153+
154+
/**
155+
* Send a handshake message. In the case of the extension host, it just sends
156+
* back a debug port.
157+
*/
158+
private sendControl(message: HandshakeMessage | { debugPort?: number } ): void {
159+
this.protocol.sendControl(VSBuffer.fromString(JSON.stringify(message)));
160+
}
161+
}

0 commit comments

Comments
 (0)