Home Reference Source Test

src/eventstore/getConnectInfo.ts

import * as dns from 'dns'
import * as util from 'util'
import * as request from 'request-promise-native'
import * as bunyan from 'bunyan'
import {JSONValue} from '../JSON'
import {URL} from 'url'
import {EventstoreSettings} from './EventstoreSettings'

/**
 * dns lookup for given domain and returns list of corresponding ip's
 */
export const getIpListFromDns = async (dnsServer: string, log: bunyan): Promise<string[]> => {
  const lookup = util.promisify(dns.lookup)
  const dnsOptions = {
    family: 4,
    hints: dns.ADDRCONFIG | dns.V4MAPPED,
    all: true
  }
  log.debug({dnsServer}, 'Fetch ip list from dns')
  let ipList: string[] = []

  try {
    const result:
      | {address: string | dns.LookupAddress[]; family?: number}
      | dns.LookupAddress[] = await lookup(dnsServer, dnsOptions)

    if (Array.isArray(result)) {
      ipList = result.map((entry: dns.LookupAddress): string => entry.address)
    }
    log.debug({dnsServer, ipList}, 'Finished dns lookup')
  } catch (err) {
    log.error({err}, 'Failed to fetch dns information')
  }

  return ipList
}

/**
 * tries to fetch gossip json information from given ip and port
 */
export const fetchgossipJson = async (
  host: string,
  port: number,
  useHttps: boolean,
  timeout: number,
  log: bunyan
): Promise<
  | JSONValue & {
      members: {
        state: string
        externalTcpIp: string
        externalTcpPort: number
        externalSecureTcpPort: number
        isAlive: boolean
      }[]
    }
  | null
> => {
  let gossipInfo = null
  const protocol = useHttps ? 'https' : 'http'
  try {
    log.debug({host, protocol, port}, 'Try to fetch gossip info')
    gossipInfo = await request.get({
      uri: `${protocol}://${host}:${port}/gossip?format=json`,
      json: true,
      timeout
    })
  } catch (err) {
    log.error({err}, 'No gossip info found')
  }
  return gossipInfo
}

/**
 * Searches for master node inside of gossip json
 */
export const getMasterNodeInfo = (
  gossipInfo: JSONValue & {
    members: {
      state: string
      externalTcpIp: string
      externalTcpPort: number
      externalSecureTcpPort: number
      isAlive: boolean
    }[]
  }
): {ip: string; tcpPort: number; tcpSecurePort: number} | null => {
  let nodeInfo = null

  const aliveList = gossipInfo.members.filter(
    (entry): boolean => entry.isAlive && entry.state.toLowerCase() === 'master'
  )
  if (aliveList.length > 0) {
    nodeInfo = {
      ip: aliveList[0].externalTcpIp,
      tcpPort: aliveList[0].externalTcpPort,
      tcpSecurePort: aliveList[0].externalSecureTcpPort
    }
  }

  return nodeInfo
}

/**
 * Gets a random cluster node from gossip json
 */
export const getRandomNodeInfo = (
  gossipInfo: JSONValue & {
    members: {
      state: string
      externalTcpIp: string
      externalTcpPort: number
      externalSecureTcpPort: number
      isAlive: boolean
    }[]
  }
): {ip: string; tcpPort: number; tcpSecurePort: number} | null => {
  let nodeInfo = null

  const aliveList = gossipInfo.members.filter(
    (entry): boolean => {
      const skipItWhen = ['manager', 'shuttingdown', 'shutdown']
      return entry.isAlive && !skipItWhen.includes(entry.state)
    }
  )
  if (aliveList.length > 0) {
    const pos = Math.floor(Math.random() * aliveList.length)
    nodeInfo = {
      ip: aliveList[pos].externalTcpIp,
      tcpPort: aliveList[pos].externalTcpPort,
      tcpSecurePort: aliveList[pos].externalSecureTcpPort
    }
  }

  return nodeInfo
}

/**
 * Updates connection information depending on given settings
 */
export const getIpAndPort = async (
  currentSettings: EventstoreSettings,
  log: bunyan
): Promise<EventstoreSettings> => {
  let gossipJson:
    | JSONValue & {
        members: {
          state: string
          externalTcpIp: string
          externalTcpPort: number
          externalSecureTcpPort: number
          isAlive: boolean
        }[]
      }
    | null = null

  if (currentSettings.uri && currentSettings.uri !== '') {
    const esUrl = new URL(currentSettings.uri)

    if (esUrl.username && esUrl.username !== '') {
      currentSettings.credentials.username = esUrl.username
    }
    if (esUrl.password && esUrl.password !== '') {
      currentSettings.credentials.password = esUrl.password
    }

    if (currentSettings.uri.toLowerCase().startsWith('tcp')) {
      //single node connection
      log.debug('Config for single node connection found')
      currentSettings.port = parseInt(esUrl.port) || 1113
      currentSettings.host = esUrl.hostname
      return currentSettings
    } else if (currentSettings.uri.toLowerCase().startsWith('discover')) {
      log.debug('Config for discover node connection found')
      //gossip
      gossipJson = await fetchgossipJson(
        esUrl.hostname,
        parseInt(esUrl.port),
        currentSettings.useHttps,
        currentSettings.gossipTimeout,
        log
      )
    }
  } else {
    //if we have a dns server we look for cluster node ip's
    if (currentSettings.clusterDns && currentSettings.clusterDns !== '') {
      const ipList = await getIpListFromDns(currentSettings.clusterDns, log)
      if (ipList.length > 0) {
        log.debug(`Found ${ipList.length} entries in DNS record`)
        //add dns lookup ip list
        const updatedList = ipList.concat(currentSettings.gossipSeeds)
        //remove duplicates from list
        currentSettings.gossipSeeds = [...new Set(updatedList)]
      }
    }

    //if we've a list of ip's we try to fetch gossipJson
    if (currentSettings.gossipSeeds.length > 0) {
      log.debug(
        `Try to find gossipJson from seed list of ${currentSettings.gossipSeeds.length} entries`
      )
      let found = false
      for (
        let x = 0, xMax = currentSettings.gossipSeeds.length;
        x < xMax && !found && x < currentSettings.maxDiscoverAttempts;
        x++
      ) {
        const res = await await fetchgossipJson(
          currentSettings.gossipSeeds[x],
          currentSettings.externalGossipPort,
          currentSettings.useHttps,
          currentSettings.gossipTimeout,
          log
        )
        if (res) {
          found = true
          gossipJson = res
        }
      }
    } else {
      log.debug('Gossip seed list empty')
    }
  }

  if (!gossipJson) {
    log.warn('Could not get any gossip info')
    return currentSettings
  }

  let nodeInfo: {ip: string; tcpPort: number; tcpSecurePort: number} | null = null

  if (currentSettings.requireMaster) {
    nodeInfo = getMasterNodeInfo(gossipJson)
    log.debug({nodeInfo}, 'Selecting master node')
  } else {
    nodeInfo = getRandomNodeInfo(gossipJson)
    log.debug({nodeInfo}, 'Selecting unspecific node')
  }

  if (nodeInfo) {
    currentSettings.host = nodeInfo.ip
    currentSettings.port = currentSettings.useSSL ? nodeInfo.tcpSecurePort : nodeInfo.tcpPort
  }

  return currentSettings
}