Skip to content

Commit 91d1135

Browse files
committed
Local relay implementation
Signed-off-by: rafasofizada <[email protected]>
1 parent 9dcf9f7 commit 91d1135

File tree

11 files changed

+715
-0
lines changed

11 files changed

+715
-0
lines changed

packages/local-relay/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# `@fitbit/local-relay`
2+
3+
> TODO: description
4+
5+
## Usage
6+
7+
```
8+
const localRelay = require('@fitbit/local-relay');
9+
10+
// TODO: DEMONSTRATE API
11+
```

packages/local-relay/package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "@fitbit/local-relay",
3+
"version": "1.8.0-pre.0",
4+
"description": "Local implementation of the Developer Relay",
5+
"author": "Fitbit, Inc.",
6+
"homepage": "https://github.com/Fitbit/developer-bridge/tree/main/packages/local-relay#readme",
7+
"license": "BSD-3-Clause",
8+
"main": "lib/local-relay.js",
9+
"publishConfig": {
10+
"registry": "http://registry.npmjs.org/"
11+
},
12+
"repository": "github:Fitbit/developer-bridge",
13+
"scripts": {
14+
"build": "rm -rf lib tsconfig.tsbuildinfo && tsc -b",
15+
"prepublishOnly": "yarn run build"
16+
},
17+
"bugs": {
18+
"url": "https://github.com/Fitbit/developer-bridge/issues"
19+
},
20+
"devDependencies": {
21+
"@babel/core": "^7.16.0",
22+
"@babel/preset-env": "^7.16.4",
23+
"@babel/preset-typescript": "^7.16.0",
24+
"@types/express": "^4.17.13",
25+
"@types/jest": "^27.0.2",
26+
"@types/node": "^16.10.2",
27+
"@types/supertest": "^2.0.11",
28+
"@types/websocket": "^1.0.4",
29+
"babel-jest": "^27.3.1",
30+
"jest": "^27.3.1",
31+
"supertest": "^6.1.6"
32+
},
33+
"dependencies": {
34+
"express": "^4.17.1",
35+
"typescript": "^4.4.3",
36+
"uuid": "^8.3.2",
37+
"websocket": "^1.0.34"
38+
},
39+
"bin": {
40+
"fitbit": "./lib/cli.js"
41+
},
42+
"files": [
43+
"/lib/!(*.test|*.spec).{js,d.ts}",
44+
"/lib/!(testUtils)**/!(*.test|*.spec).{js,d.ts}",
45+
"/lib/**/*.json"
46+
],
47+
"engines": {
48+
"node": ">=8.6.0"
49+
}
50+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
enum CloseCode {
2+
// These codes are specified by the RFC
3+
// https://tools.ietf.org/html/rfc6455#section-7.4.1
4+
GoingAway = 1001,
5+
PolicyViolation = 1008,
6+
}
7+
8+
export default CloseCode;

packages/local-relay/src/Config.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import { join } from "path";
4+
5+
export function loadConfiguration(configPath: string) {
6+
const config: Record<string, string> = {};
7+
8+
const configFile = fs.readFileSync(configPath).toString();
9+
for (const line of configFile.split(os.EOL)) {
10+
let [key, value] = line.split("=", 2);
11+
if (!key || !value) {
12+
continue;
13+
}
14+
15+
// tslint:disable-next-line no-param-reassign
16+
key = key.trim().toUpperCase();
17+
value = value.trim();
18+
config[key] = value;
19+
}
20+
21+
return config;
22+
}
23+
24+
export const CONTAINER_VERSION = process.env.DOCKER_TAG || "no-docker-tag";
25+
26+
export const maxPayloadSizeCap = 1024 * 1024;
27+
28+
export function validateMaxPayloadSize(maxPayload: any) {
29+
const parsedMaxPayload = parseInt(maxPayload, 10);
30+
if (!maxPayload || parsedMaxPayload > maxPayloadSizeCap) {
31+
return maxPayloadSizeCap;
32+
}
33+
34+
// tslint:disable-next-line triple-equals
35+
if (parsedMaxPayload != maxPayload) {
36+
throw new Error("MAX_PAYLOAD must be a valid number");
37+
}
38+
39+
return parsedMaxPayload;
40+
}
41+
42+
const settingsPath = process.env.SETTINGS_FILE;
43+
const config = settingsPath ? loadConfiguration(settingsPath) : {};
44+
45+
const propertiesPath = process.env.PROPERTIES_FILE;
46+
Object.assign(config, propertiesPath ? loadConfiguration(propertiesPath) : {});
47+
48+
export const signalUrl = config.SIGNAL_URL || "http://localhost:9001";
49+
export const maxPayload = validateMaxPayloadSize(config.MAX_PAYLOAD);
50+
51+
export const relayWsUrlPrefix =
52+
config.RELAY_WS_URL_PREFIX || "wss://027-v3-api-soa.fitbit.com/dbridge";
53+
54+
export const relayPkgName = "@fitbit/local-developer-relay";
55+
export const relayDirectoryName = "fitbit-local-relay";
56+
export const relayDirectoryPath = join(os.tmpdir(), relayDirectoryName);
57+
58+
export const relayPidFileName = "pid.json";
59+
export const relayPidFilePath = join(relayDirectoryPath, relayPidFileName);
60+
61+
export const relayLogFileName = "logs.txt";
62+
export const relayLogFilePath = join(relayDirectoryPath, relayLogFileName);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as websocket from "websocket";
2+
3+
import CloseCode from "./CloseCode";
4+
import { maxPayload } from "./Config";
5+
6+
export default class Connection {
7+
private slavePeer?: websocket.connection;
8+
9+
constructor(private masterPeer: websocket.connection) {
10+
this.masterPeer.on("message", this.onMasterMessage);
11+
this.masterPeer.on("close", this.onMasterClose);
12+
}
13+
14+
public isHalfOpen(): boolean {
15+
return !this.slavePeer;
16+
}
17+
18+
public connectPeer(slavePeer: websocket.connection): void {
19+
this.slavePeer = slavePeer;
20+
this.slavePeer.on("message", this.onSlaveMessage);
21+
this.slavePeer.on("close", this.onSlaveClose);
22+
this.sendHostHello();
23+
}
24+
25+
public close(code: CloseCode): void {
26+
this.masterPeer.close(code);
27+
if (this.slavePeer) {
28+
this.slavePeer.close(code);
29+
}
30+
}
31+
32+
private sendHostHello(): void {
33+
this.masterPeer.send(
34+
JSON.stringify({
35+
maxPayload,
36+
relayEvent: "connection",
37+
})
38+
);
39+
}
40+
41+
private forwardMessage(
42+
destination: websocket.connection | undefined,
43+
data: websocket.Message
44+
) {
45+
let payload;
46+
if (data.type === "utf8") {
47+
payload = data.utf8Data!;
48+
} else if (data.type === "binary") {
49+
payload = data.binaryData!;
50+
} else {
51+
console.error(`Invalid payload type: ${(data as any).type}`);
52+
this.close(CloseCode.PolicyViolation);
53+
return;
54+
}
55+
56+
if (destination) {
57+
destination.send(payload);
58+
} else {
59+
this.close(CloseCode.PolicyViolation);
60+
}
61+
}
62+
63+
private onMasterMessage = (data: websocket.Message) => {
64+
this.forwardMessage(this.slavePeer, data);
65+
};
66+
67+
private onSlaveMessage = (data: websocket.Message) =>
68+
this.forwardMessage(this.masterPeer, data);
69+
70+
private forwardClose(
71+
destination: websocket.connection | undefined,
72+
code: number,
73+
message: string
74+
) {
75+
if (destination) {
76+
const skipCloseFrame =
77+
code === websocket.connection.CLOSE_REASON_ABNORMAL;
78+
if (skipCloseFrame) {
79+
destination.drop(code, message, true);
80+
} else {
81+
destination.close(code, message);
82+
}
83+
}
84+
}
85+
86+
private onMasterClose = (code: number, message: string) => {
87+
this.forwardClose(this.slavePeer, code, message);
88+
};
89+
90+
private onSlaveClose = (code: number, message: string) => {
91+
this.forwardClose(this.masterPeer, code, message);
92+
};
93+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as websocket from "websocket";
2+
3+
import CloseCode from "./CloseCode";
4+
import Connection from "./Connection";
5+
6+
export default class ConnectionManager {
7+
private connections = new Map<string, Connection>();
8+
9+
public canConnectSlave(connectionID: string) {
10+
const master = this.connections.get(connectionID);
11+
return !!master && master.isHalfOpen();
12+
}
13+
14+
public connectMaster(connectionID: string, ws: websocket.connection): void {
15+
if (this.connections.has(connectionID)) {
16+
throw new Error(`Connection ID ${connectionID} collided`);
17+
}
18+
19+
ws.on("close", this.onClose.bind(this, connectionID));
20+
21+
this.connections.set(connectionID, new Connection(ws));
22+
}
23+
24+
public connectSlave(
25+
connectionID: string,
26+
webSocket: websocket.connection
27+
): void {
28+
const existingConnection = this.connections.get(connectionID);
29+
if (!existingConnection) {
30+
throw new Error(`No connection ID '${connectionID}'`);
31+
}
32+
33+
existingConnection.connectPeer(webSocket);
34+
}
35+
36+
public close(connectionID: string) {
37+
const connection = this.connections.get(connectionID);
38+
if (connection) {
39+
connection.close(CloseCode.GoingAway);
40+
} else {
41+
console.info(`Connection ID '${connectionID}' is unknown; cannot close`);
42+
}
43+
}
44+
45+
private onClose(connectionID: string): void {
46+
console.info(`Connection ID '${connectionID}' was closed`);
47+
48+
const connection = this.connections.get(connectionID);
49+
if (connection) {
50+
} else {
51+
console.error(
52+
`Connection ID '${connectionID}' was not found while trying to close it.`
53+
);
54+
}
55+
56+
this.connections.delete(connectionID);
57+
}
58+
}

packages/local-relay/src/Host.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as websocket from "websocket";
2+
import CloseCode from "./CloseCode";
3+
import Connection from "./Connection";
4+
5+
export type HostInfo = {
6+
id: string;
7+
displayName: string;
8+
roles: string[];
9+
};
10+
11+
export default class Host {
12+
private _id: string;
13+
14+
get id(): string {
15+
return this._id;
16+
}
17+
18+
private _displayName: string;
19+
20+
get displayName(): string {
21+
return this._displayName;
22+
}
23+
24+
private _roles: string[];
25+
26+
get roles(): string[] {
27+
return this._roles;
28+
}
29+
30+
private connection?: Connection;
31+
32+
constructor({ id, displayName, roles }: HostInfo) {
33+
this._id = id;
34+
this._displayName = displayName;
35+
this._roles = roles;
36+
}
37+
38+
connect(ws: websocket.connection): void {
39+
ws.on("close", () => {
40+
this.connection = undefined;
41+
});
42+
43+
this.connection = new Connection(ws);
44+
}
45+
46+
isConnected(): boolean {
47+
return Boolean(this.connection);
48+
}
49+
50+
disconnect(): void {
51+
if (!this.connection) {
52+
console.info(
53+
`Host ${this._id} is not connected; cannot close connection`
54+
);
55+
56+
return;
57+
}
58+
59+
this.connection.close(CloseCode.GoingAway);
60+
}
61+
62+
connectPeer(peerWs: websocket.connection) {
63+
if (!this.connection) {
64+
throw new Error(`Host ${this._id} is not connected`);
65+
}
66+
67+
if (!this.canConnectPeer()) {
68+
throw new Error(`Host ${this._id} is already connected to a peer`);
69+
}
70+
71+
this.connection.connectPeer(peerWs);
72+
}
73+
74+
canConnectPeer() {
75+
return this.connection.isHalfOpen();
76+
}
77+
}

0 commit comments

Comments
 (0)