Source

index.js

/**
 * The Missing (TCP_KEEPINTVL and TCP_KEEPCNT) SO_KEEPALIVE socket option setters and getters for Node using
 * ffi-rs module.
 *
 * Note: For methods provided by this module to work you must enable SO_KEEPALIVE and set the TCP_KEEPIDLE options for
 * socket using Net.Socket-s built in method socket.setKeepAlive([enable][, initialDelay]) !
 *
 * @author George Kotchlamazashvili <georgedot@gmail.com>
 *
 * @example <caption>CommonJS</caption>
 * require('net-keepalive')
 *
 * @example <caption>ES Modules</caption>
 * import * as NetKeepalive from 'net-keepalive'
 *
 * @module net-keepalive
 * @since v0.1.0
 */
'use strict'

const Assert = require('assert'),
  Net = require('net'),
  OS = require('os'),
  Constants = require('./constants'),
  { DataType, createPointer, restorePointer } = require('ffi-rs'),
  FFIBindings = require('./ffi-bindings')

const _isSocket = (socket) => socket instanceof Net.Socket

const _getSocketFD = (socket) => {
  const fd = socket._handle != null ? socket._handle.fd : undefined
  if (OS.platform() !== 'win32') {
    Assert(fd && fd !== -1, 'Unable to get socket fd')
  }
  return fd
}

/**
 * Sets the TCP_KEEPINTVL value for specified socket.
 *
 * Note: The msec will be rounded towards the closest integer
 *
 * @since v0.1.0
 * @param {Net.Socket} socket to set the value for
 * @param {number} msecs to wait in-between probes
 *
 * @returns {boolean} <code>true</code> on success
 *
 * @example <caption>Set interval in-between probes to 1 second (<code>1000</code> milliseconds) for socket <code>s</code></caption>
 * NetKeepAlive.setKeepAliveInterval(s, 1000)
 *
 * @throws {AssertionError} setKeepAliveInterval requires two arguments
 * @throws {AssertionError} setKeepAliveInterval expects an instance of socket as its first argument
 * @throws {AssertionError} setKeepAliveInterval requires msec to be a Number
 * @throws {ErrnoException|Error} Unexpected error
 */
module.exports.setKeepAliveInterval = function setKeepAliveInterval(
  socket,
  msecs
) {
  Assert.strictEqual(
    arguments.length,
    2,
    'setKeepAliveInterval requires two arguments'
  )
  Assert(
    _isSocket(socket),
    'setKeepAliveInterval expects an instance of socket as its first argument'
  )
  Assert.strictEqual(
    msecs != null ? msecs.constructor : void 0,
    Number,
    'setKeepAliveInterval requires msec to be a Number'
  )

  const fd = _getSocketFD(socket),
    seconds = ~~(msecs / 1000),
    dataType = DataType.I32,
    [intvlVal] = createPointer({
      paramsType: [dataType],
      paramsValue: [seconds],
    }),
    intvlValLn = 4 // sizeof int

  return FFIBindings.setsockopt(
    fd,
    Constants.SOL_TCP,
    Constants.TCP_KEEPINTVL,
    intvlVal,
    intvlValLn
  )
}

/**
 * Returns the TCP_KEEPINTVL value for specified socket.
 *
 * @since v1.1.0
 * @param {Net.Socket} socket to check the value for
 *
 * @returns {number} interval (ms) in-between probes
 *
 * @example <caption>Get the current interval in-between probes for socket <code>s</code></caption>
 * NetKeepAlive.getKeepAliveInterval(s) // returns 1000 based on setter example
 *
 * @throws {AssertionError} getKeepAliveInterval requires one arguments
 * @throws {AssertionError} getKeepAliveInterval expects an instance of socket as its first argument
 * @throws {ErrnoException|Error} Unexpected error
 */
module.exports.getKeepAliveInterval = function getKeepAliveInterval(socket) {
  Assert.strictEqual(
    arguments.length,
    1,
    'getKeepAliveInterval requires one arguments'
  )
  Assert(
    _isSocket(socket),
    'getKeepAliveInterval expects an instance of socket as its first argument'
  )

  const fd = _getSocketFD(socket),
    dataType = DataType.I32,
    [intvlVal] = createPointer({
      paramsType: [dataType],
      paramsValue: [0],
    }),
    [intvlValLn] = createPointer({
      paramsType: [DataType.I32],
      paramsValue: [4], // sizeof int
    })

  FFIBindings.getsockopt(
    fd,
    Constants.SOL_TCP,
    Constants.TCP_KEEPINTVL,
    intvlVal,
    intvlValLn
  )

  const [value] = restorePointer({
    retType: [dataType],
    paramsValue: [intvlVal],
  })

  return value * 1000
}

/**
 * Sets the TCP_KEEPCNT value for specified socket.
 *
 * @since v0.1.0
 * @param {Net.Socket} socket to set the value for
 * @param {number} cnt number of probes to send
 *
 * @returns {boolean} <code>true</code> on success
 *
 *
 * @example <caption>Set number of probes to send to 1 for socket <code>s</code></caption>
 * NetKeepAlive.setKeepAliveProbes(s, 1)
 *
 * @throws {AssertionError} setKeepAliveProbes requires two arguments
 * @throws {AssertionError} setKeepAliveProbes expects an instance of socket as its first argument
 * @throws {AssertionError} setKeepAliveProbes requires cnt to be a Number
 * @throws {ErrnoException|Error} Unexpected error
 */
module.exports.setKeepAliveProbes = function setKeepAliveProbes(socket, cnt) {
  Assert.strictEqual(
    arguments.length,
    2,
    'setKeepAliveProbes requires two arguments'
  )
  Assert(
    _isSocket(socket),
    'setKeepAliveProbes expects an instance of socket as its first argument'
  )
  Assert.strictEqual(
    cnt != null ? cnt.constructor : void 0,
    Number,
    'setKeepAliveProbes requires cnt to be a Number'
  )

  const fd = _getSocketFD(socket),
    count = ~~cnt,
    dataType = DataType.I32,
    [cntVal] = createPointer({
      paramsType: [dataType],
      paramsValue: [count],
    }),
    cntValLn = 4 // sizeof int

  return FFIBindings.setsockopt(
    fd,
    Constants.SOL_TCP,
    Constants.TCP_KEEPCNT,
    cntVal,
    cntValLn
  )
}

/**
 * Returns the TCP_KEEPCNT value for specified socket.
 *
 * @since v1.1.0
 * @param {Net.Socket} socket to check the value for
 *
 * @returns {number} number of probes to send
 *
 * @example <caption>Get the current number of probes to send for socket <code>s</code></caption>
 * NetKeepAlive.getKeepAliveProbes(s) // returns 1 based on setter example
 *
 * @throws {AssertionError} getKeepAliveProbes requires one arguments
 * @throws {AssertionError} getKeepAliveProbes expects an instance of socket as its first argument
 * @throws {ErrnoException|Error} Unexpected error
 */
module.exports.getKeepAliveProbes = function getKeepAliveProbes(socket) {
  Assert.strictEqual(
    arguments.length,
    1,
    'getKeepAliveProbes requires one arguments'
  )
  Assert(
    _isSocket(socket),
    'getKeepAliveProbes expects an instance of socket as its first argument'
  )

  const fd = _getSocketFD(socket),
    dataType = DataType.I32,
    [cntVal] = createPointer({
      paramsType: [dataType],
      paramsValue: [0],
    }),
    [cntValLn] = createPointer({
      paramsType: [DataType.I32],
      paramsValue: [4], // sizeof int
    })

  FFIBindings.getsockopt(
    fd,
    Constants.SOL_TCP,
    Constants.TCP_KEEPCNT,
    cntVal,
    cntValLn
  )

  const [value] = restorePointer({
    retType: [dataType],
    paramsValue: [cntVal],
  })

  return value
}

/**
 * Sets the TCP_USER_TIMEOUT (TCP_RXT_CONNDROPTIME on `darwin`) value for specified socket.
 *
 * Notes:
 * * This method is only supported on `linux` and `darwin`, will throw on `freebsd`.
 * * The msec will be rounded towards the closest integer.
 * * When used with the TCP keepalive options, will override them.
 *
 * @see {@link https://man7.org/linux/man-pages/man7/tcp.7.html|tcp(7):TCP_USER_TIMEOUT }
 *
 * @since v1.4.0
 * @param {Net.Socket} socket to set the value for
 * @param {number} msecs to wait for unacknowledged data before closing the connection
 *
 * @returns {boolean} <code>true</code> on success
 *
 * @example <caption>Set user timeout to 30 seconds (<code>30000</code> milliseconds) for socket <code>s</code></caption>
 * NetKeepAlive.setUserTimeout(s, 30000)
 *
 * @throws {AssertionError} setUserTimeout requires two arguments
 * @throws {AssertionError} setUserTimeout called on unsupported platform
 * @throws {AssertionError} setUserTimeout expects an instance of socket as its first argument
 * @throws {AssertionError} setUserTimeout requires msec to be a Number
 * @throws {ErrnoException|Error} Unexpected error
 */
module.exports.setUserTimeout = function setUserTimeout(socket, msecs) {
  Assert.strictEqual(
    arguments.length,
    2,
    'setUserTimeout requires two arguments'
  )
  Assert(
    Constants.TCP_USER_TIMEOUT != null,
    'setUserTimeout called on unsupported platform'
  )
  Assert(
    _isSocket(socket),
    'setUserTimeout expects an instance of socket as its first argument'
  )
  Assert.strictEqual(
    msecs != null ? msecs.constructor : void 0,
    Number,
    'setUserTimeout requires msec to be a Number'
  )

  const fd = _getSocketFD(socket),
    msecInt = ~~msecs,
    dataType = DataType.I32,
    [msecVal] = createPointer({
      paramsType: [dataType],
      paramsValue: [msecInt],
    }),
    msecValLn = 4 // sizeof int

  return FFIBindings.setsockopt(
    fd,
    Constants.SOL_TCP,
    Constants.TCP_USER_TIMEOUT,
    msecVal,
    msecValLn
  )
}

/**
 * Returns the TCP_USER_TIMEOUT value for specified socket.
 *
 * Note: This method is only supported on `linux`, will throw on `darwin` and `freebsd`.
 *
 * @since v1.4.0
 * @param {Net.Socket} socket to check the value for
 *
 * @returns {number} msecs to wait for unacknowledged data before closing the connection
 *
 * @example <caption>Get the current user timeout for socket <code>s</code></caption>
 * NetKeepAlive.getUserTimeout(s) // returns 30000 based on setter example
 *
 * @throws {AssertionError} getUserTimeout requires one arguments
 * @throws {AssertionError} getUserTimeout called on unsupported platform
 * @throws {AssertionError} getUserTimeout expects an instance of socket as its first argument
 * @throws {ErrnoException|Error} Unexpected error
 */
module.exports.getUserTimeout = function getUserTimeout(socket) {
  Assert.strictEqual(
    arguments.length,
    1,
    'getUserTimeout requires one arguments'
  )
  Assert(
    Constants.TCP_USER_TIMEOUT != null,
    'getUserTimeout called on unsupported platform'
  )
  Assert(
    _isSocket(socket),
    'getUserTimeout expects an instance of socket as its first argument'
  )

  const fd = _getSocketFD(socket),
    dataType = DataType.I32,
    [msecVal] = createPointer({
      paramsType: [dataType],
      paramsValue: [0],
    }),
    [msecValLn] = createPointer({
      paramsType: [DataType.I32],
      paramsValue: [4], // sizeof int
    })

  FFIBindings.getsockopt(
    fd,
    Constants.SOL_TCP,
    Constants.TCP_USER_TIMEOUT,
    msecVal,
    msecValLn
  )

  const [value] = restorePointer({
    retType: [dataType],
    paramsValue: [msecVal],
  })

  return value
}