Skip to content

Commit

Permalink
feat(core): convert server to use websockets
Browse files Browse the repository at this point in the history
  • Loading branch information
blakebyrnes committed Jan 7, 2021
1 parent 4c9025e commit 2d1804c
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 71 deletions.
2 changes: 1 addition & 1 deletion client/connections/CoreClientConnection.ts
Expand Up @@ -19,7 +19,7 @@ export default abstract class CoreClientConnection {
public readonly commandQueue: CoreCommandQueue;
public options: ICoreConnectionOptions;

protected connectPromise: Promise<Error | null>;
private connectPromise: Promise<Error | null>;

private coreSessions: CoreSessions;
private readonly pendingRequestsById = new Map<string, IResolvablePromiseWithId>();
Expand Down
56 changes: 31 additions & 25 deletions client/connections/RemoteCoreConnection.ts
@@ -1,65 +1,71 @@
import JsonSocket from 'json-socket';
import Net from 'net';
import ICoreRequestPayload from '@secret-agent/core-interfaces/ICoreRequestPayload';
import { URL } from 'url';
import WebSocket from 'ws';
import CoreClientConnection from './CoreClientConnection';
import ICoreConnectionOptions from '../interfaces/ICoreConnectionOptions';

export default class RemoteCoreConnection extends CoreClientConnection {
protected netConnectPromise: Promise<any>;
private netSocket: Net.Socket;
private jsonSocket: JsonSocket;
private wsConnectPromise: Promise<any>;
private webSocket: WebSocket;

constructor(options: ICoreConnectionOptions) {
super(options);
if (!options.host) throw new Error('A remote core connection needs a host parameter!');
}

public internalSendRequest(payload: ICoreRequestPayload): Promise<void> {
const message = JSON.stringify(payload);
return new Promise((resolve, reject) =>
this.jsonSocket.sendMessage(payload, err => {
this.webSocket.send(message, err => {
if (err) reject(err);
else resolve();
}),
);
}

public async disconnect(): Promise<void> {
if (this.netSocket && !this.netSocket.destroyed) {
if (
this.webSocket &&
this.webSocket.readyState !== WebSocket.CLOSED &&
this.webSocket.readyState !== WebSocket.CLOSING
) {
await super.disconnect();
await new Promise<void>(resolve => {
this.netSocket.end(() => process.nextTick(resolve));
});
this.webSocket.terminate();
}
this.netSocket = null;
this.netConnectPromise = null;
this.webSocket = null;
}

public connect(): Promise<Error | null> {
if (!this.netConnectPromise) {
this.netConnectPromise = this.netConnect().catch(err => err);
if (!this.wsConnectPromise) {
this.wsConnectPromise = this.wsConnect().catch(err => err);
}

return this.netConnectPromise;
return this.wsConnectPromise;
}

public isRemoteConnection(): boolean {
return false;
}

private async netConnect(): Promise<void> {
private async wsConnect(): Promise<void> {
let host = this.options.host;
if (!host.includes('://')) {
host = `tcp://${host}`;
host = `ws://${host}`;
}
const parsedUrl = new URL(host);
const connect = { host: parsedUrl.hostname, port: parseInt(parsedUrl.port, 10) };
this.netSocket = Net.connect(connect);
await new Promise<void>(resolve => this.netSocket.once('connect', resolve));

this.netSocket.once('close', this.disconnect.bind(this));
this.jsonSocket = new JsonSocket(this.netSocket);
this.jsonSocket.on('message', payload => this.onMessage(payload));
this.webSocket = new WebSocket(host);
await new Promise<void>((resolve, reject) => {
this.webSocket.on('error', reject);
this.webSocket.once('open', () => {
this.webSocket.off('error', reject);
resolve();
});
});
this.webSocket.once('close', this.disconnect.bind(this));
this.webSocket.on('message', message => {
const payload = JSON.parse(message.toString());
this.onMessage(payload);
});

await super.connect();
}
}
2 changes: 1 addition & 1 deletion client/package.json
Expand Up @@ -13,7 +13,7 @@
"@secret-agent/core-interfaces": "1.2.0-alpha.4",
"@secret-agent/replay": "1.2.0-alpha.4",
"awaited-dom": "^1.1.10",
"json-socket": "^0.3.0",
"ws": "^7.4.2",
"uuid": "^8.1.0"
},
"devDependencies": {
Expand Down
78 changes: 43 additions & 35 deletions core/lib/RemoteServer.ts
@@ -1,38 +1,45 @@
import JsonSocket from 'json-socket';
import Net, { AddressInfo, NetConnectOpts } from 'net';
import { AddressInfo, ListenOptions } from 'net';
import WebSocket from 'ws';
import Log from '@secret-agent/commons/Logger';
import * as http from 'http';
import * as url from 'url';
import Core from '../index';

const { log } = Log(module);

export default class RemoteServer {
public get port() {
return (this.netServer.address() as AddressInfo).port;
return (this.httpServer.address() as AddressInfo).port;
}

private readonly netServer: Net.Server;
private netConnectionsById: { [id: string]: Net.Socket } = {};
private lastConnectionId = 0;
public get hasConnections() {
return this.wsServer.clients.size > 0;
}

private readonly wsServer: WebSocket.Server;
private readonly httpServer: http.Server;

constructor() {
this.netServer = Net.createServer(this.handleNetConnection.bind(this));
this.httpServer = new http.Server();
this.wsServer = new WebSocket.Server({ server: this.httpServer });
this.wsServer.on('connection', this.handleConnection);
}

public listen(options: NetConnectOpts): Promise<AddressInfo> {
public listen(options: ListenOptions): Promise<AddressInfo> {
return new Promise<AddressInfo>((resolve, reject) => {
this.netServer.on('error', reject);
this.netServer.listen(options, () => {
this.netServer.off('error', reject);
resolve(this.netServer.address() as AddressInfo);
this.httpServer.on('error', reject);
this.httpServer.listen(options, () => {
this.httpServer.off('error', reject);
resolve(this.httpServer.address() as AddressInfo);
});
});
}

public close(): Promise<void> {
return new Promise<void>(resolve => {
try {
const promises = Object.values(this.netConnectionsById).map(netSocket => netSocket.end());
this.netServer.close(async () => {
const promises = [...this.wsServer.clients].map(ws => ws.terminate());
this.httpServer.close(async () => {
await Promise.all(promises);
setImmediate(resolve);
});
Expand All @@ -46,29 +53,30 @@ export default class RemoteServer {
});
}

private handleNetConnection(netConnection: Net.Socket): void {
const jsonSocket = new JsonSocket(netConnection);
const coreConnection = Core.addConnection();
const id = (this.lastConnectionId += 1).toString();
private handleConnection(ws: WebSocket, request: http.IncomingMessage): void {
const requestUrl = url.parse(request.url);

this.netConnectionsById[id] = netConnection;

coreConnection.on('message', payload => {
jsonSocket.sendMessage(payload, error => {
if (error) {
log.error('Error sending message', {
error,
sessionId: null,
});
jsonSocket.destroy(error);
}
if (requestUrl.pathname === '/') {
const coreConnection = Core.addConnection();
ws.on('message', message => {
const payload = JSON.parse(message.toString());
return coreConnection.handleRequest(payload);
});
});
jsonSocket.on('message', payload => coreConnection.handleRequest(payload));

jsonSocket.on('end', () => {
delete this.netConnectionsById[id];
coreConnection.disconnect();
});
coreConnection.on('message', payload => {
const json = JSON.stringify(payload);
ws.send(json, error => {
if (error) {
log.error('Error sending message', {
error,
sessionId: null,
});
ws.close(500, JSON.stringify({ message: error.message }));
}
});
});
} else if (requestUrl.pathname === '/replay') {
// tbd:
}
}
}
2 changes: 1 addition & 1 deletion core/package.json
Expand Up @@ -20,7 +20,7 @@
"moment": "^2.24.0",
"regenerator-runtime": "^0.13.7",
"typeson": "^5.18.2",
"json-socket": "^0.3.0",
"ws": "^7.4.2",
"typeson-registry": "^1.0.0-alpha.38",
"uuid": "^8.1.0"
},
Expand Down
4 changes: 2 additions & 2 deletions core/test/remote.test.ts
Expand Up @@ -10,7 +10,7 @@ beforeAll(async () => {
httpServer = await Helpers.runHttpServer({ onlyCloseOnFinal: true });
remoteServer = new RemoteServer();
Helpers.onClose(() => remoteServer.close(), true);
await remoteServer.listen({ port: 0, host: '127.0.0.1' });
await remoteServer.listen({ port: 0 });
});
afterAll(Helpers.afterAll);
afterEach(Helpers.afterEach);
Expand All @@ -20,7 +20,7 @@ describe('basic remote connection tests', () => {
// bind a core server to core

const handler = new Handler({
host: `127.0.0.1:${remoteServer.port}`,
host: `ws://127.0.0.1:${remoteServer.port}`,
});
const handlerAgent = await handler.createAgent();
const sessionId = await handlerAgent.sessionId;
Expand Down
2 changes: 1 addition & 1 deletion website/docs/Advanced/Remote.md
@@ -1,6 +1,6 @@
# Remote

SecretAgent comes out of the box ready to act as a remote process that can communicate over a tcp socket to a client.
SecretAgent comes out of the box ready to act as a remote process that can communicate over a WebSocket to a client.

You'll need a simple script to start the server on the machine where the `secret-agent` npm package is installed. Make sure to open the port you allocate on any firewall that a client might have to pass through:

Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Expand Up @@ -11720,11 +11720,6 @@ json-schema@0.2.3:
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=

json-socket@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/json-socket/-/json-socket-0.3.0.tgz#f4b953c685bb8e8bd0b72438f5208d9a0799ae07"
integrity sha512-jc8ZbUnYIWdxERFWQKVgwSLkGSe+kyzvmYxwNaRgx/c8NNyuHes4UHnPM3LUrAFXUx1BhNJ94n1h/KCRlbvV0g==

json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
Expand Down Expand Up @@ -19334,6 +19329,11 @@ ws@^7.2.3, ws@^7.3.0, ws@^7.3.1:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==

ws@^7.4.2:
version "7.4.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==

x-is-string@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
Expand Down

0 comments on commit 2d1804c

Please sign in to comment.