Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
Make dashboard bind to IPv4 & IPv6 when available
Browse files Browse the repository at this point in the history
Addresses #5690
  • Loading branch information
benjamincburns committed Nov 21, 2022
1 parent 3950dca commit 4713cc5
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 39 deletions.
48 changes: 29 additions & 19 deletions packages/dashboard-message-bus/lib/DashboardMessageBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ interface UnfulfilledRequest {
}

export class DashboardMessageBus extends EventEmitter {
private publishServer: WebSocket.Server;
private subscribeServer: WebSocket.Server;
private publishServers: WebSocket.Server[];
private subscribeServers: WebSocket.Server[];
private publishers: WebSocket[] = [];
private subscribers: WebSocket[] = [];
private readyPromise: Promise<void>;
Expand All @@ -42,32 +42,36 @@ export class DashboardMessageBus extends EventEmitter {
* @dev This starts separate websocket servers for subscribers/publishers
*/
async start() {
this.subscribeServer = await startWebSocketServer({
this.subscribeServers = await startWebSocketServer({
host: this.host,
port: this.subscribePort
});

this.subscribeServer.on("connection", (newSubscriber: WebSocket) => {
newSubscriber.once("close", () => {
this.removeSubscriber(newSubscriber);
});
this.subscribeServers.map(subscribeServer =>
subscribeServer.on("connection", (newSubscriber: WebSocket) => {
newSubscriber.once("close", () => {
this.removeSubscriber(newSubscriber);
});

// Require the subscriber to send a message *first* before being added
newSubscriber.once("message", () => this.addSubscriber(newSubscriber));
});
// Require the subscriber to send a message *first* before being added
newSubscriber.once("message", () => this.addSubscriber(newSubscriber));
})
);

this.publishServer = await startWebSocketServer({
this.publishServers = await startWebSocketServer({
host: this.host,
port: this.publishPort
});

this.publishServer.on("connection", (newPublisher: WebSocket) => {
newPublisher.once("close", () => {
this.removePublisher(newPublisher);
});
this.publishServers.map(publishServer =>
publishServer.on("connection", (newPublisher: WebSocket) => {
newPublisher.once("close", () => {
this.removePublisher(newPublisher);
});

this.addPublisher(newPublisher);
});
this.addPublisher(newPublisher);
})
);
}

/**
Expand All @@ -83,8 +87,14 @@ export class DashboardMessageBus extends EventEmitter {
* @dev Emits a "terminate" event
*/
async terminate() {
await promisify(this.publishServer.close.bind(this.publishServer))();
await promisify(this.subscribeServer.close.bind(this.subscribeServer))();
await Promise.all([
...this.publishServers.map(publishServer =>
promisify(publishServer.close.bind(publishServer))()
),
...this.subscribeServers.map(subscribeServer =>
promisify(subscribeServer.close.bind(subscribeServer))()
)
]);
this.emit("terminate");
}

Expand Down
43 changes: 37 additions & 6 deletions packages/dashboard-message-bus/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {
jsonToBase64,
base64ToJson
} from "@truffle/dashboard-message-bus-common";

import { networkInterfaces } from "os";
import { isIP } from "net";
import { promises as dns } from "dns";

import any from "promise.any";

any.shim();
Expand All @@ -13,12 +18,20 @@ any.shim();
* @dev If you need to attach event listeners *before* the server connection opens,
* do not use this function since it resolves *after* the connection is opened
*/
export const startWebSocketServer = (options: ServerOptions) => {
return new Promise<WebSocket.Server>(resolve => {
const server = new WebSocket.Server(options, () => {
resolve(server);
});
});
export const startWebSocketServer = async (options: ServerOptions) => {
const ipsForHost = await resolveBindHostnameToAllIps(
options.host ?? "localhost"
);
return Promise.all(
ipsForHost.map(
ip =>
new Promise<WebSocket.Server>(resolve => {
const server = new WebSocket.Server({ ...options, host: ip }, () => {
resolve(server);
});
})
)
);
};

/**
Expand Down Expand Up @@ -79,3 +92,21 @@ export const sendAndAwait = (socket: WebSocket, message: Message) => {
socket.send(encodedMessage);
});
};

const getLocalInterfaceAddresses = () => {
return Object.keys(networkInterfaces()).flatMap(name =>
networkInterfaces()[name].map(iface => iface.address)
);
};

export const resolveBindHostnameToAllIps = async (hostname: string) => {
if (isIP(hostname) !== 0) {
return [hostname];
}

const interfaces = getLocalInterfaceAddresses();
const results = await dns.lookup(hostname, { all: true });
return results
.map(result => result.address)
.filter(address => interfaces.includes(address));
};
42 changes: 28 additions & 14 deletions packages/dashboard/lib/DashboardServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { DashboardMessageBus } from "@truffle/dashboard-message-bus";
import { DashboardMessageBusClient } from "@truffle/dashboard-message-bus-client";
import cors from "cors";
import type { Server } from "http";
import { createServer } from "http";
import debugModule from "debug";
import { resolveBindHostnameToAllIps } from "./utils";

export interface DashboardServerOptions {
/** Port of the dashboard */
Expand Down Expand Up @@ -46,7 +48,7 @@ export class DashboardServer {
frontendPath: string;

private expressApp?: Application;
private httpServer?: Server;
private httpServers: Server[] = [];
private messageBus?: DashboardMessageBus;
private client?: DashboardMessageBusClient;
private configPublishPort?: number;
Expand Down Expand Up @@ -76,7 +78,8 @@ export class DashboardServer {
}

async start() {
if (this.httpServer?.listening) {
// if we've initialized the httpServers array, we're already listening.
if (this.httpServers.length > 0) {
return;
}

Expand All @@ -99,15 +102,26 @@ export class DashboardServer {
res.sendFile("index.html", { root: this.frontendPath });
});

await new Promise<void>(resolve => {
this.httpServer = this.expressApp!.listen(this.port, this.host, () => {
if (this.autoOpen) {
const host = this.host === "0.0.0.0" ? "localhost" : this.host;
open(`http://${host}:${this.port}`);
}
resolve();
});
});
const bindIpAddresses = await resolveBindHostnameToAllIps(this.host);
this.httpServers = await Promise.all(
bindIpAddresses.map(async bindIpAddress => {
const server = createServer(this.expressApp);
return new Promise<Server>(resolve => {
server.listen(
{
host: bindIpAddress,
port: this.port
},
() => resolve(server)
);
});
})
);

if (this.autoOpen) {
const host = this.host === "0.0.0.0" ? "localhost" : this.host;
open(`http://${host}:${this.port}`);
}
}

async stop() {
Expand All @@ -116,9 +130,9 @@ export class DashboardServer {
await Promise.all([
this.client?.close(),
this.messageBus?.terminate(),
new Promise<void>(resolve => {
this.httpServer?.close(() => resolve());
})
...this.httpServers.map(
server => new Promise(resolve => server.close(resolve))
)
]);
delete this.client;
}
Expand Down
21 changes: 21 additions & 0 deletions packages/dashboard/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { jsonToBase64 } from "@truffle/dashboard-message-bus-common";
import { spawn } from "child_process";
import type { DashboardServerOptions } from "./DashboardServer";
import { isIP } from "net";
import { promises as dns } from "dns";
import { networkInterfaces } from "os";
import path from "path";

export const startDashboardInBackground = (options: DashboardServerOptions) => {
Expand All @@ -15,3 +18,21 @@ export const startDashboardInBackground = (options: DashboardServerOptions) => {

return child;
};

const getLocalInterfaceAddresses = () => {
return Object.keys(networkInterfaces()).flatMap(name =>
networkInterfaces()[name].map(iface => iface.address)
);
};

export const resolveBindHostnameToAllIps = async (hostname: string) => {
if (isIP(hostname) !== 0) {
return [hostname];
}

const interfaces = getLocalInterfaceAddresses();
const results = await dns.lookup(hostname, { all: true });
return results
.map(result => result.address)
.filter(address => interfaces.includes(address));
};

0 comments on commit 4713cc5

Please sign in to comment.