All files / lib PingPongState.ts

100% Statements 40/40
100% Branches 16/16
100% Functions 10/10
100% Lines 39/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 1391x 1x     1x 59x 59x 59x 59x     59x                                           59x               12x                     15x     15x 9x 9x     9x       15x 3x                 13x 13x             8x     8x     8x 8x     8x         8x 8x         11x     11x       8x           3x     3x 1x 1x 1x         1x 1x       9x 1x 1x        
import { PingMessage } from "./messages/PingMessage";
import { PongMessage } from "./messages/PongMessage";
import { Peer } from "./Peer";
 
export class PingPongState {
    public PING_INTERVAL_MS: number = 60000;
    public PONG_TIMEOUT_MS: number = 15000;
    public PING_FLOOD_THRESHOLD: number = 10;
    public PONG_REQUIRED_THRESHOLD: number = 65532;
 
    private _peerClient: Peer;
    private _pingsReceieved: number = 0;
    private _lastMessageReceived: any;
    private _sendPingIntervalHandle: NodeJS.Timeout;
    private _pongTimeoutHandle: NodeJS.Timeout;
    private _sentPing: PingMessage;
 
    /**
     * Maintains ping/pong state for a client where by it will perform several functions. Refer to
     * Bolt01 for all nuances of implementation.
     *
     * 0. Upon receipt of a message from the remote server, we reset the ping timeout as
     *   we are aware that the server is still live.
     *
     * 1. When there are no messages from a client for a period of time, it will emit a ping
     *   and wait for the pong.  If not pong is received, or the pong is invalid, then the
     *   connection is terminated.
     *
     * 2. When a ping is received an appropriate pong message will be sent. If pong messages
     *   are received more frequently than 30 second, then they are ignored. If more than
     *   5 pings are receiced in a 30 second period, then we will close the connection.
     */
    constructor(peerClient: Peer) {
        this._peerClient = peerClient;
    }
 
    /**
     * Starts the PingPongState manager by starting a ping interval that will
     * consider sending a ping every 60s
     */
    public start() {
        this._sendPingIntervalHandle = setInterval(
            this._onSendPingInterval.bind(this),
            this.PING_INTERVAL_MS,
        );
    }
 
    /**
     * Handles incoming messages
     */
    public onMessage(m: any) {
        // update the time of the last received message
        this._lastMessageReceived = Date.now();
 
        // received ping
        if (m.type === 18) {
            this._pingsReceieved += 1;
            this._checkForPingFlood();
 
            // only send pong when num_pong_bytes as per spec
            if (m.numPongBytes < this.PONG_REQUIRED_THRESHOLD) this._sendPong(m);
        }
 
        // recieved pong
        if (m.type === 19) {
            this._validatePong(m);
        }
    }
 
    /**
     * Fires prior to the peer being disconnected
     * and will clean up resources
     */
    public onDisconnecting() {
        clearTimeout(this._pongTimeoutHandle);
        clearInterval(this._sendPingIntervalHandle);
    }
 
    ///////////
 
    private _sendPing() {
        // clear existing pong timeout handle in case we have yet to receive a pong
        clearTimeout(this._pongTimeoutHandle);
 
        // create the timeout we will wait for the pong
        this._pongTimeoutHandle = setTimeout(this._pongTimedOut.bind(this), this.PONG_TIMEOUT_MS);
 
        // create and send the ping
        const ping = new PingMessage();
        this._peerClient.sendMessage(ping);
 
        // capture this so we can validate it
        this._sentPing = ping;
    }
 
    private _sendPong(ping) {
        // construct and send a message
        const pong = new PongMessage(ping.numPongBytes);
        this._peerClient.sendMessage(pong);
    }
 
    private _onSendPingInterval() {
        // reset the number of pings received
        this._pingsReceieved = 0;
 
        // if message has been received within a minute, then do not send a ping
        if (
            !this._lastMessageReceived ||
            Date.now() - this._lastMessageReceived > this.PING_INTERVAL_MS
        ) {
            this._sendPing();
        }
    }
 
    private _validatePong(pong) {
        // clear the pong timeout
        clearTimeout(this._pongTimeoutHandle);
 
        // check that pong is a valid one and if not, we disconnect
        if (this._sentPing && this._sentPing.numPongBytes !== pong.ignored.length) {
            this._peerClient.logger.debug("invalid pong message");
            this._peerClient.disconnect();
            return;
        }
    }
 
    private _pongTimedOut() {
        this._peerClient.logger.debug("timed out waiting for pong");
        this._peerClient.reconnect();
    }
 
    private _checkForPingFlood() {
        if (this._pingsReceieved > this.PING_FLOOD_THRESHOLD) {
            this._peerClient.logger.debug("ping flooding detected");
            this._peerClient.disconnect();
        }
    }
}