2021-10-02 00:16:34 +00:00
|
|
|
"use strict";
|
|
|
|
|
2021-10-09 17:00:54 +00:00
|
|
|
import { EventEmitter, once } from 'events';
|
|
|
|
import net from 'net';
|
|
|
|
|
2022-01-08 16:59:08 +00:00
|
|
|
import p_retry from 'p-retry';
|
2022-01-08 21:37:03 +00:00
|
|
|
import log from 'log'; const l = log.get("peerserver");
|
2022-01-08 16:59:08 +00:00
|
|
|
|
2021-10-09 17:00:54 +00:00
|
|
|
import Connection from '../transport/Connection.mjs';
|
2021-10-19 01:36:22 +00:00
|
|
|
import ErrorWrapper from '../core/ErrorWrapper.mjs';
|
2021-10-09 17:00:54 +00:00
|
|
|
|
|
|
|
import Peer from './Peer.mjs';
|
|
|
|
|
2021-10-19 01:43:55 +00:00
|
|
|
/**
|
|
|
|
* A server that handles connections to many peers.
|
2021-12-27 18:34:44 +00:00
|
|
|
* Note that when a new peer connects it is NOT asked for a list of peers it is
|
|
|
|
* aware of. This is something you need to handle yourself!
|
2021-10-19 01:43:55 +00:00
|
|
|
* @extends EventEmitter
|
|
|
|
*/
|
2021-10-09 17:00:54 +00:00
|
|
|
class PeerServer extends EventEmitter {
|
2021-10-19 01:36:22 +00:00
|
|
|
constructor(our_id, secret_join) {
|
2021-10-09 17:00:54 +00:00
|
|
|
super();
|
|
|
|
|
|
|
|
this.our_id = our_id;
|
2021-10-19 01:36:22 +00:00
|
|
|
this.secret_join = secret_join;
|
2021-10-02 00:16:34 +00:00
|
|
|
|
2022-01-08 16:59:08 +00:00
|
|
|
// The number of retries when attempting to connect to a peer
|
|
|
|
this.retries = 5;
|
|
|
|
|
2021-10-19 01:36:22 +00:00
|
|
|
this.connected_peers = [];
|
|
|
|
this.connecting_peers = [];
|
2021-10-09 17:00:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts the PeerServer listening on the given port and bind address.
|
|
|
|
* @param {Number} [port=5252] The port number to listen on.
|
|
|
|
* @param {String} [host="::"] The address to bind to.
|
|
|
|
* @return {Promise<void>} A Promise that resolves when the server setup is complete.
|
|
|
|
*/
|
|
|
|
listen(port = 5252, host="::") {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.server = net.createServer(async (client) => {
|
|
|
|
await this.handle_client(client);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.server.once("error", reject);
|
|
|
|
this.server.on("error", this.handle_error);
|
|
|
|
this.server.listen({
|
|
|
|
host,
|
|
|
|
port,
|
|
|
|
exclusive: false
|
|
|
|
}, () => {
|
|
|
|
this.server.off("error", reject);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handle_error(error) {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
async handle_client(client) {
|
2021-10-19 01:36:22 +00:00
|
|
|
const peer = await Peer.Accept(this, await Connection.Wrap(this.secret_join, client));
|
2022-01-08 16:59:08 +00:00
|
|
|
this.peer_initialise(peer);
|
|
|
|
await once(peer, "connect");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialises a CONNECTED peer and registers it as a valid peer.
|
|
|
|
* This also includesd attaching the necessary event handlers.
|
|
|
|
* @param {Peer} peer The peer in question.
|
|
|
|
* @return {void}
|
|
|
|
*/
|
|
|
|
peer_initialise(peer) {
|
2021-10-19 01:36:22 +00:00
|
|
|
this.connected_peers.push(peer);
|
2021-10-09 17:00:54 +00:00
|
|
|
peer.on("message", this.handle_message.bind(this, peer));
|
|
|
|
peer.on("destroy", this.handle_destroy.bind(this, peer));
|
|
|
|
}
|
|
|
|
|
|
|
|
async handle_message(peer, message) {
|
|
|
|
this.emit("message", peer, message);
|
|
|
|
this.emit(`message-${message.event}`, peer, message.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
async handle_destroy(peer) {
|
2021-10-19 01:36:22 +00:00
|
|
|
const index = this.connected_peers.indexOf(peer);
|
2021-10-09 17:00:54 +00:00
|
|
|
if(index > -1)
|
2021-10-19 01:36:22 +00:00
|
|
|
this.connected_peers.splice(index, 1);
|
2021-10-09 17:00:54 +00:00
|
|
|
|
2022-01-08 16:59:08 +00:00
|
|
|
l.log(`Peer ${peer.address}:${peer.port} disconnected`);
|
2021-10-09 17:00:54 +00:00
|
|
|
this.emit("disconnect", peer.remote_endpoint);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a list of all currently known peer addresses.
|
|
|
|
* @return {{address:string,port:number}[]}
|
|
|
|
*/
|
2021-10-19 01:36:22 +00:00
|
|
|
peers() {
|
|
|
|
return this.connected_peers.map((peer) => peer.remote_endpoint)
|
2021-10-09 17:00:54 +00:00
|
|
|
.filter(el => typeof el.addr === "string" && typeof el.port === "number");
|
|
|
|
}
|
|
|
|
|
2022-01-08 16:59:08 +00:00
|
|
|
/**
|
|
|
|
* Resolves a Peer id to the respective peer instance.
|
|
|
|
* @param {string|Peer} peer_id The peer ID to resolve as a string. If a Peer instance is passed instead, this is simply returned unchanged.
|
|
|
|
* @return {Peer} The Peer instance associated with the given peer id.
|
|
|
|
*/
|
|
|
|
peer_resolve(peer_id) {
|
|
|
|
if(peer_id instanceof Peer) return peer_id;
|
|
|
|
for (let peer of this.connected_peers) {
|
|
|
|
if(peer.id === peer_id) return peer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolves a list of peer ids (and potentially Peer instances) to a list
|
|
|
|
* of Peer instances.
|
|
|
|
* Any Peer instances passed are returned unchanged.
|
|
|
|
* @param {...string|Peer} peers The peer(s) to resolve.
|
|
|
|
* @return {Peer[]} A list of Peer instances.
|
|
|
|
*/
|
|
|
|
peers_resolve(...peers) {
|
|
|
|
return peers.map(this.peer_resolve);
|
|
|
|
}
|
|
|
|
|
2021-10-19 01:36:22 +00:00
|
|
|
/**
|
|
|
|
* Processes a list of peers.
|
|
|
|
* New connections are established to any peers in the list to which
|
|
|
|
* we don't already have a connection.
|
|
|
|
* Note that this function does NOT connect to any other peers known to the
|
|
|
|
* peers in the list you've specified! You need to do this manually.
|
|
|
|
* @param {...{address: string, port: number}} new_peers The list of new peers to process.
|
|
|
|
* @returns {Promise<Peer[]>} A list of new peers to which we have successfully established a connection.
|
|
|
|
*/
|
|
|
|
async add_peers(...new_peers) {
|
2022-01-08 16:59:08 +00:00
|
|
|
// The arrow function here is NOT async because the promise from
|
|
|
|
// this.add_peer is returned directly to await Promise.all().
|
|
|
|
return (await Promise.all(new_peers.map(
|
|
|
|
new_peer => p_retry(async () => await this.add_peer(
|
|
|
|
new_peer.address,
|
|
|
|
new_peer.port
|
2022-01-08 17:29:09 +00:00
|
|
|
), {
|
|
|
|
retries: this.retries,
|
|
|
|
onFailedAttempt: (error, attempt, left) =>
|
|
|
|
l.error(`[attempt ${attempt} / ${left}] Error while connecting to ${new_peer.address}:${new_peer.port}: ${error}`)
|
|
|
|
})
|
2022-01-08 16:59:08 +00:00
|
|
|
))).filter(peer => peer instanceof Peer);
|
2021-10-19 01:36:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connects to a new peer and adds them to the pool of currently connected peers.
|
|
|
|
* Note: This does NOT automatically connect to all the peers known to the
|
|
|
|
* peer you're connecting to!
|
|
|
|
* You need to do this manually.
|
|
|
|
* @throws {ErrorWrapper} Throws if the connection failed. This could be for a large number of different reasons, from an incorrect join secret from the remote to connection issues.
|
|
|
|
* @param {string} address The address to connect to.
|
|
|
|
* @param {number} port The port number to connect to.
|
|
|
|
* @return {Promise<Peer|null>} A Promise that resolves to the resulting Peer connection, or null if the connection wasn't attemped.
|
|
|
|
*/
|
|
|
|
async add_peer(address, port) {
|
2022-01-08 16:59:08 +00:00
|
|
|
// If we're already connected, don't bother reconnecting again
|
2021-10-19 01:36:22 +00:00
|
|
|
if(this.peers().some(el => el.address === address && el.port === port))
|
|
|
|
return;
|
|
|
|
|
|
|
|
const peer_string = `peer:${address}:${port}`;
|
|
|
|
this.connecting_peers.push(peer_string);
|
|
|
|
let conn;
|
|
|
|
try {
|
|
|
|
conn = await Peer.Initiate(this, address, port);
|
2022-01-08 16:59:08 +00:00
|
|
|
peer_initialise(conn);
|
2021-10-19 01:36:22 +00:00
|
|
|
}
|
|
|
|
catch(error) {
|
|
|
|
throw new ErrorWrapper(`Error: Failed to connect to peer.`, error);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
this.connecting_peers.splice(this.connecting_peers.indexOf(peer_string));
|
|
|
|
}
|
|
|
|
|
|
|
|
this.emit(`peer`, conn);
|
|
|
|
return conn;
|
|
|
|
}
|
2021-10-09 17:00:54 +00:00
|
|
|
|
2021-10-19 01:43:55 +00:00
|
|
|
/**
|
|
|
|
* Sends a message to 1 or more peers.
|
|
|
|
* @param {string|Peer|string[]|Peer[]} peer_id Either the peer id or the peer itself to which we should send the message. May also be an array of arbitrarily mixed items - in which case the message will be sent to all the specified peers in parallel. The order which peers are messaged is undefined.
|
|
|
|
* @param {string} event_name The name of the event to send.
|
|
|
|
* @param {Object} msg The message itself to send.
|
|
|
|
* @return {Promise} A Promise that resolves (or potentially rejects) when the message has been sent.
|
|
|
|
*/
|
|
|
|
async send(peer_id, event_name, msg) {
|
2022-01-08 16:59:08 +00:00
|
|
|
if(!(peer_id instanceof Array)) peer_id = [ peer_id ];
|
|
|
|
|
|
|
|
await Promise.all(this.peers_resolve(...peer_id).map(
|
|
|
|
peer => peer.send(event_name, msg)
|
|
|
|
));
|
2021-10-19 01:43:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a message in parallel to all peers to which we have an established
|
|
|
|
* connection.
|
|
|
|
* The order which peers are messaged is undefined.
|
|
|
|
* @param {string} event_name The name of the event to send.
|
|
|
|
* @param {Object} msg The message itself to send.
|
|
|
|
* @return {Promise} A Promise that resolves (or potentially rejects) when the message has been sent.
|
|
|
|
*/
|
|
|
|
async broadcast(event_name, msg) {
|
|
|
|
await this.send(this.connected_peers, event_name, msg);
|
|
|
|
}
|
|
|
|
|
2021-10-09 17:00:54 +00:00
|
|
|
/**
|
|
|
|
* Shuts the server down.
|
|
|
|
* This does not disconnect any existing peers!
|
|
|
|
* @return {Promise} A Promise that resolves once the server has been shutdown.
|
|
|
|
*/
|
|
|
|
shutdown_server() {
|
|
|
|
return new Promise((resolve, _reject) => {
|
|
|
|
this.server.close(resolve);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the PeerServer and gracefully (if possible) disconnects all existing peers.
|
|
|
|
* @return {Promise} A Promise that resolves once the shutdown process has completed.
|
|
|
|
*/
|
|
|
|
async destroy() {
|
|
|
|
await this.shutdown_server();
|
2021-10-19 01:36:22 +00:00
|
|
|
await Promise.all(...this.connected_peers.map(peer => peer.destroy()));
|
2021-10-02 00:16:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default PeerServer;
|