Skip to content
Open
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