Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local relay implementation #182

Open
wants to merge 14 commits into
base: feature/local-relay
Choose a base branch
from
3 changes: 2 additions & 1 deletion greenkeeper.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"packages/fdb-debugger/package.json",
"packages/fdb-host/package.json",
"packages/fdb-protocol/package.json",
"packages/sdk-cli/package.json"
"packages/sdk-cli/package.json",
"packages/local-relay/package.json"
]
}
}
Expand Down
11 changes: 11 additions & 0 deletions packages/local-relay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `@fitbit/local-relay`

> TODO: description

## Usage

```
const localRelay = require('@fitbit/local-relay');

// TODO: DEMONSTRATE API
```
5 changes: 5 additions & 0 deletions packages/local-relay/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = require('../jest.config.base')({
clearMocks: true,
restoreMocks: true,
displayName: require('./package.json').name,
});
49 changes: 49 additions & 0 deletions packages/local-relay/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@fitbit/local-relay",
"version": "1.8.0-pre.0",
"description": "Local implementation of the Developer Relay",
"author": "Fitbit, Inc.",
"homepage": "https://github.com/Fitbit/developer-bridge/tree/main/packages/local-relay#readme",
"license": "BSD-3-Clause",
"main": "lib/index.js",
"publishConfig": {
"registry": "http://registry.npmjs.org/"
},
"repository": "github:Fitbit/developer-bridge",
"scripts": {
"build": "rm -rf lib tsconfig.tsbuildinfo && tsc -b",
"prepublishOnly": "yarn run build"
},
"bugs": {
"url": "https://github.com/Fitbit/developer-bridge/issues"
},
"devDependencies": {
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@types/express": "^4.17.13",
"@types/jest": "^27.0.2",
"@types/node": "^16.10.2",
"@types/supertest": "^2.0.11",
"@types/websocket": "^1.0.4",
"babel-jest": "^27.3.1",
"jest": "^27.3.1",
"supertest": "^6.1.6"
},
"dependencies": {
"express": "^4.17.1",
"typescript": "^4.4.3",
"uuid": "^8.3.2",
"websocket": "^1.0.34"
},
"bin": {
"fitbit": "./lib/cli.js"
},
"files": [
"/lib/!(*.test|*.spec).{js,d.ts}",
"/lib/!(testUtils)**/!(*.test|*.spec).{js,d.ts}",
"/lib/**/*.json"
],
"engines": {
"node": ">=12.0.0"
}
}
8 changes: 8 additions & 0 deletions packages/local-relay/src/CloseCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
enum CloseCode {
// These codes are specified by the RFC
// https://tools.ietf.org/html/rfc6455#section-7.4.1
GoingAway = 1001,
PolicyViolation = 1008,
}

export default CloseCode;
11 changes: 11 additions & 0 deletions packages/local-relay/src/Config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as os from 'os';
import { join } from 'path';

export const maxPayload = 1024 * 1024;

export const relayPkgName = '@fitbit/local-developer-relay';
export const relayDirectoryName = 'fitbit-local-relay';
export const relayDirectoryPath = join(os.tmpdir(), relayDirectoryName);

export const relayPidFileName = 'pid.json';
export const relayPidFilePath = join(relayDirectoryPath, relayPidFileName);
168 changes: 168 additions & 0 deletions packages/local-relay/src/Connection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import * as websocket from 'websocket';
import { EventEmitter } from 'events';

import CloseCode from './CloseCode';
import Connection from './Connection';

describe('Connection', () => {
describe('isHalfOpen', () => {
it('true if no peer connected', () => {
const connection = new Connection(
new EventEmitter() as websocket.connection,
);

expect(connection.isHalfOpen()).toBe(true);
});

it('false if connected to peer', () => {
const masterPeer = new EventEmitter() as websocket.connection;
masterPeer.send = jest.fn();

const connection = new Connection(masterPeer);

expect(connection.isHalfOpen()).toBe(true);
connection.connectPeer(new EventEmitter() as websocket.connection);
expect(connection.isHalfOpen()).toBe(false);
});
});

describe('forwards message to peer', () => {
it('utf8', (done) =>
messageTest({ type: 'utf8', utf8Data: 'test' }, 'test', done));

it('binary', (done) => {
const binaryData = Buffer.from('imaginary binary data');
messageTest({ binaryData, type: 'binary' }, binaryData, done);
});

it('neither (error)', (done) => {
const consoleSpy = jest.spyOn(console, 'error');
const closeSpy = jest
.spyOn(Connection.prototype, 'close')
.mockImplementation();

// done() won't get called
messageTest({ type: 'other', data: '' } as any, undefined, done);
expect(consoleSpy).toBeCalledWith('Invalid payload type: other');
expect(closeSpy).toBeCalledWith(CloseCode.PolicyViolation);
return done();
});

// TODO: Jest doesn't recognize the "else destination" branch in forwardMessage
it('no peer (error)', () => {
const masterPeer = new EventEmitter() as websocket.connection;
const connection = new Connection(masterPeer);
connection.close = jest.fn();

masterPeer.emit(
'message',
websocket.connection.CLOSE_REASON_NOT_PROVIDED,
'',
);

expect(connection.close).toBeCalledWith(CloseCode.PolicyViolation);
});
});

describe('forwards close event to peer', () => {
it('drop', (done) => {
const args: [number, string] = [
websocket.connection.CLOSE_REASON_ABNORMAL,
'test',
];
closeTest(...args, 'drop', [...args, true], done);
});

it('close', (done) => {
const args: [number, string] = [
websocket.connection.CLOSE_REASON_NOT_PROVIDED,
'test',
];
closeTest(...args, 'close', args, done);
});
});

describe('close', () => {
it('closes both master and slave peers', () => {
const masterPeer = ({
on: jest.fn(),
close: jest.fn(),
} as unknown) as websocket.connection;
const connection = new Connection(masterPeer);

const slavePeer = ({
close: jest.fn(),
} as unknown) as websocket.connection;
connection['slavePeer'] = slavePeer;

const code = websocket.connection.CLOSE_REASON_NOT_PROVIDED;
connection.close(code);
expect(masterPeer.close).toHaveBeenCalledWith(code);
expect(slavePeer.close).toHaveBeenCalledWith(code);
});
});

describe('peer', () => {
it('forwards messages to master (utf8)', (done) => {
const masterPeer = new EventEmitter() as websocket.connection;

const payload = { type: 'utf8', utf8Data: 'test' };
masterPeer.send = (data) => {
expect(data).toEqual(payload.utf8Data);
return done();
};

const connection = new Connection(masterPeer);
connection['sendHostHello'] = () => {};

const slavePeer = new EventEmitter() as websocket.connection;
connection.connectPeer(slavePeer);

slavePeer.emit('message', payload);
});
});
});

function messageTest(
payload: websocket.Message,
receivedData: any,
done: jest.DoneCallback,
) {
const masterPeer = new EventEmitter() as websocket.connection;
const connection = new Connection(masterPeer);

const peerSendFn = jest.fn().mockImplementation((payload: any) => {
expect(payload).toEqual(receivedData);
return done();
});

connection['slavePeer'] = ({
send: peerSendFn,
} as unknown) as websocket.connection;

masterPeer.emit('message', payload);
}

function closeTest(
code: number,
message: string,
expectedCloseFn: 'drop' | 'close',
expectedArgs: any[],
done: jest.DoneCallback,
) {
const masterPeer = new EventEmitter() as websocket.connection;
const connection = new Connection(masterPeer);

const peerCloseFn = jest
.fn()
.mockImplementation((...args: [number, string, boolean]) => {
expect(args).toEqual(expectedArgs);
return done();
});

connection['slavePeer'] = ({
[expectedCloseFn]: peerCloseFn,
} as unknown) as websocket.connection;

masterPeer.emit('close', code, message);
}
91 changes: 91 additions & 0 deletions packages/local-relay/src/Connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as websocket from 'websocket';

import CloseCode from './CloseCode';
import { maxPayload } from './Config';

export default class Connection {
private slavePeer?: websocket.connection;

constructor(private masterPeer: websocket.connection) {
this.masterPeer.on('message', this.onMasterMessage);
this.masterPeer.on('close', this.onMasterClose);
}

public isHalfOpen(): boolean {
return !this.slavePeer;
}

public connectPeer(slavePeer: websocket.connection): void {
this.slavePeer = slavePeer;
this.slavePeer.on('message', this.onSlaveMessage);
this.slavePeer.on('close', this.onSlaveClose);
this.sendHostHello();
}

public close(code: CloseCode): void {
this.masterPeer.close(code);
this.slavePeer?.close(code);
}

private sendHostHello(): void {
this.masterPeer.send(
JSON.stringify({
maxPayload,
relayEvent: 'connection',
}),
);
}

private forwardMessage(
destination: websocket.connection | undefined,
data: websocket.Message,
) {
let payload;
if (data.type === 'utf8') {
payload = data.utf8Data!;
} else if (data.type === 'binary') {
payload = data.binaryData!;
} else {
console.error(`Invalid payload type: ${(data as any).type}`);
this.close(CloseCode.PolicyViolation);
return;
}

if (destination) {
destination.send(payload);
} else {
this.close(CloseCode.PolicyViolation);
}
}

private onMasterMessage = (data: websocket.Message) => {
this.forwardMessage(this.slavePeer, data);
};

private onSlaveMessage = (data: websocket.Message) =>
this.forwardMessage(this.masterPeer, data);

private forwardClose(
destination: websocket.connection | undefined,
code: number,
message: string,
) {
if (destination) {
const skipCloseFrame =
code === websocket.connection.CLOSE_REASON_ABNORMAL;
if (skipCloseFrame) {
destination.drop(code, message, true);
} else {
destination.close(code, message);
}
}
}

private onMasterClose = (code: number, message: string) => {
this.forwardClose(this.slavePeer, code, message);
};

private onSlaveClose = (code: number, message: string) => {
this.forwardClose(this.masterPeer, code, message);
};
}
Loading