All files / lib PingPongState.ts

7.5% Statements 3/40
0% Branches 0/16
0% Functions 0/10
7.69% Lines 3/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 139 140 141 1421x 1x     1x                                                                                                                                                                                                                                                                                  
import { PingMessage } from './messages/PingMessage';
import { PongMessage } from './messages/PongMessage';
import { Peer } from './Peer';
 
export class PingPongState {
  public PING_INTERVAL_MS = 60000;
  public PONG_TIMEOUT_MS = 15000;
  public PING_FLOOD_THRESHOLD = 10;
  public PONG_REQUIRED_THRESHOLD = 65532;
 
  private _peerClient: Peer;
  private _pingsReceieved = 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();
    }
  }
}