src/main/generic/network/websocket/WebSocketConnector.js
class WebSocketConnector extends Observable {
/**
* @constructor
* @param {number} protocol
* @param {string} protocolPrefix
* @param {NetworkConfig} networkConfig
* @listens WebSocketServer#connection
*/
constructor(protocol, protocolPrefix, networkConfig) {
super();
this._protocol = protocol;
this._protocolPrefix = protocolPrefix;
this._networkConfig = networkConfig;
if (networkConfig.peerAddress.protocol === this._protocol) {
this._wss = WebSocketFactory.newWebSocketServer(networkConfig);
this._wss.on('connection', (ws, req) => this._onConnection(ws, req));
Log.d(WebSocketConnector, `${this._protocolPrefix.toUpperCase()}-Connector listening on port ${networkConfig.peerAddress.port}`);
}
/** @type {HashMap.<PeerAddress, WebSocket>} */
this._sockets = new HashMap();
/** @type {Timers} */
this._timers = new Timers();
}
/**
* @fires WebSocketConnector#connection
* @fires WebSocketConnector#error
* @param {PeerAddress} peerAddress
* @returns {boolean}
*/
connect(peerAddress) {
if (peerAddress.protocol !== this._protocol) throw new Error('Malformed peerAddress');
const timeoutKey = `connect_${peerAddress}`;
if (this._timers.timeoutExists(timeoutKey)) {
Log.w(WebSocketConnector, `Already connecting to ${peerAddress}`);
return false;
}
const ws = WebSocketFactory.newWebSocket(`${this._protocolPrefix}://${peerAddress.host}:${peerAddress.port}`, {
handshakeTimeout: WebSocketConnector.CONNECT_TIMEOUT
}, this._networkConfig);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
this._timers.clearTimeout(timeoutKey);
this._sockets.remove(peerAddress);
// Don't fire error events after the connection has been established.
ws.onerror = () => {};
// There is no way to determine the remote IP in the browser ... thanks for nothing, WebSocket API.
const netAddress = (ws._socket && ws._socket.remoteAddress) ? NetAddress.fromIP(ws._socket.remoteAddress, true) : null;
const conn = new NetworkConnection(new WebSocketDataChannel(ws), this._protocol, netAddress, peerAddress);
this.fire('connection', conn);
};
ws.onerror = e => {
this._timers.clearTimeout(timeoutKey);
this._sockets.remove(peerAddress);
/**
* Tell listeners that an error has ocurred.
* @event WebSocketConnector#error
*/
this.fire('error', peerAddress, e);
};
this._sockets.put(peerAddress, ws);
this._timers.setTimeout(timeoutKey, () => {
this._timers.clearTimeout(timeoutKey);
this._sockets.remove(peerAddress);
// We don't want to fire the error event again if the websocket
// connect fails at a later time.
ws.onerror = () => {};
// If the connection succeeds after we have fired the error event,
// close it.
ws.onopen = () => {
Log.d(WebSocketConnector, () => `Connection to ${peerAddress} succeeded after timeout - closing it`);
ws.close();
};
/**
* Tell listeners that a timeout error has occurred.
* @event WebSocketConnector#error
*/
this.fire('error', peerAddress, 'timeout');
}, WebSocketConnector.CONNECT_TIMEOUT);
return true;
}
/**
* @param {PeerAddress} peerAddress
* @fires WebSocketConnector#error
* @returns {void}
*/
abort(peerAddress) {
const ws = this._sockets.get(peerAddress);
if (!ws) {
return;
}
this._timers.clearTimeout(`connect_${peerAddress}`);
this._sockets.remove(peerAddress);
ws.onerror = () => {};
ws.onopen = () => {
Log.d(WebSocketConnector, () => `Connection to ${peerAddress} succeeded after aborting - closing it`);
ws.close();
};
ws.close();
/**
* Tell listeners that the connection attempt has been aborted.
* @event WebSocketConnector#error
*/
this.fire('error', peerAddress, 'aborted');
}
/**
* @fires WebSocketConnector#connection
* @param {WebSocket} ws
* @param {http.IncomingMessage} req
* @returns {void}
*/
_onConnection(ws, req) {
let remoteAddress = req.connection.remoteAddress;
if (!remoteAddress) {
Log.e(WebSocketConnector, 'Expected req.connection.remoteAddress to be set and it is not: closing the connection');
ws.close();
return;
}
// If we're behind a reverse proxy, the peer's IP will be in the header set by the reverse proxy, not in the req.connection object
if (this._networkConfig.reverseProxy.enabled) {
const reverseProxyAddresses = this._networkConfig.reverseProxy.addresses;
if (reverseProxyAddresses.includes(remoteAddress)) {
const reverseProxyHeader = this._networkConfig.reverseProxy.header;
if (req.headers[reverseProxyHeader]) {
remoteAddress = req.headers[reverseProxyHeader].split(/\s*,\s*/)[0];
} else {
Log.e(WebSocketConnector, `Expected header '${reverseProxyHeader}' to contain the real IP from the connecting client: closing the connection`);
ws.close();
return;
}
} else {
Log.e(WebSocketConnector, `Received connection from ${remoteAddress} when all connections were expected from the reverse proxy at ${reverseProxyAddresses}: closing the connection`);
ws.close();
return;
}
}
try {
const netAddress = NetAddress.fromIP(remoteAddress, true);
const conn = new NetworkConnection(new WebSocketDataChannel(ws), this._protocol, netAddress, /*peerAddress*/ null);
/**
* Tell listeners that an initial connection to a peer has been established.
* @event WebSocketConnector#connection
*/
this.fire('connection', conn);
} catch (e) {
Log.e(WebSocketConnector, `Error on connection from ${remoteAddress}: ${e.message || e}`);
ws.close();
}
}
}
WebSocketConnector.CONNECT_TIMEOUT = 1000 * 5; // 5 seconds
Class.register(WebSocketConnector);