haraka/test-fixtures

View on GitHub
lib/connection.js

Summary

Maintainability
C
1 day
Test Coverage
const config = require('haraka-config')
const constants = require('haraka-constants')
const Notes = require('haraka-notes')
const ResultStore = require('haraka-results')
const utils = require('haraka-utils')

const logger = require('./logger')
const transaction = require('./transaction')

const states = constants.connection.state

class Connection {
  constructor(client, server, cfg) {
    this.client = client
    this.server = server
    this.cfg = cfg

    this.local = {
      ip: null,
      port: null,
      host: 'haraka-test.example.com',
      info: 'Haraka',
    }
    this.remote = {
      ip: '127.0.0.1',
      port: null,
      host: null,
      info: null,
      closed: false,
      is_private: false,
      is_local: false,
    }
    this.hello = {
      host: null,
      verb: null,
    }
    this.tls = {
      enabled: false,
      advertised: false,
      verified: false,
      cipher: {},
    }
    this.proxy = {
      allowed: false,
      ip: null,
      type: null,
      timer: null,
    }
    this.set('tls', 'enabled', !!server.has_tls)

    this.current_data = null
    this.current_line = null
    this.state = states.PAUSE
    this.encoding = 'utf8'
    this.prev_state = null
    this.loop_code = null
    this.loop_msg = null
    this.uuid = utils.uuid()
    this.notes = new Notes()
    this.transaction = null
    this.tran_count = 0
    this.capabilities = null
    this.ehlo_hello_message = 'Haraka Test is at your service.'
    this.connection_close_message = 'closing test connection.'
    this.banner_includes_uuid = true
    this.deny_includes_uuid = true
    this.early_talker = false
    this.pipelining = false
    this._relaying = false
    this.esmtp = false
    this.last_response = null
    this.hooks_to_run = []
    this.start_time = Date.now()
    this.last_reject = ''
    this.max_bytes = 0
    this.max_mime_parts = 1000
    this.totalbytes = 0
    this.rcpt_count = {
      accept: 0,
      tempfail: 0,
      reject: 0,
    }
    this.msg_count = {
      accept: 0,
      tempfail: 0,
      reject: 0,
    }
    this.max_line_length = 512
    this.max_data_line_length = 992
    this.results = new ResultStore(this)
    this.errors = 0
    this.last_rcpt_msg = null
    this.hook = null
    if (this.cfg.headers.show_version) {
      this.local.info += `/${utils.getVersion(__dirname)}`
    }

    logger.add_log_methods(this, 'mock-connection')
    Connection.setupClient(this)
  }

  static setupClient(self) {
    if (Object.keys(self.client).length === 0) return
    const ip = self.client.remoteAddress
    if (!ip) {
      self.logdebug('setupClient got no IP address for this connection!')
      self.client.destroy()
      return
    }

    const local_addr = self.server.address()
    self.set(
      'local',
      'ip',
      ipaddr.process(self.client.localAddress || local_addr.address).toString(),
    )
    self.set('local', 'port', self.client.localPort || local_addr.port)
    self.results.add({ name: 'local' }, self.local)

    self.set('remote', 'ip', ipaddr.process(ip).toString())
    self.set('remote', 'port', self.client.remotePort)
    self.results.add({ name: 'remote' }, self.remote)

    self.lognotice('connect', {
      ip: self.remote.ip,
      port: self.remote.port,
      local_ip: self.local.ip,
      local_port: self.local.port,
    })

    if (!self.client.on) return

    const log_data = { ip: self.remote.ip }
    if (self.remote.host) log_data.host = self.remote.host

    self.client.on('end', () => {
      if (self.state >= states.DISCONNECTING) return
      self.remote.closed = true
      self.loginfo('client half closed connection', log_data)
      self.fail()
    })

    self.client.on('close', (has_error) => {
      if (self.state >= states.DISCONNECTING) return
      self.remote.closed = true
      self.loginfo('client dropped connection', log_data)
      self.fail()
    })

    self.client.on('error', (err) => {
      if (self.state >= states.DISCONNECTING) return
      self.loginfo(`client connection error: ${err}`, log_data)
      self.fail()
    })

    self.client.on('timeout', () => {
      // FIN has sent, when timeout just destroy socket
      if (self.state >= states.DISCONNECTED) {
        self.client.destroy()
        self.loginfo(`timeout, destroy socket (state:${self.state})`)
        return
      }
      if (self.state >= states.DISCONNECTING) return
      self.respond(421, 'timeout', () => {
        self.fail('client connection timed out', log_data)
      })
    })

    self.client.on('data', (data) => {
      self.process_data(data)
    })

    plugins.run_hooks('connect_init', self)
  }

  setTLS(obj) {
    this.set('hello', 'host', undefined)
    this.set('tls', 'enabled', true)
    for (const t of ['cipher', 'verified', 'verifyError', 'peerCertificate']) {
      if (obj[t] === undefined) return
      this.set('tls', t, obj[t])
    }
  }

  set(prop_str, val) {
    if (arguments.length === 3) {
      prop_str = `${arguments[0]}.${arguments[1]}`
      val = arguments[2]
    }

    const path_parts = prop_str.split('.')
    let loc = this
    for (let i = 0; i < path_parts.length; i++) {
      const part = path_parts[i]
      if (part === '__proto__' || part === 'constructor') continue

      // while another part remains
      if (i < path_parts.length - 1) {
        if (loc[part] === undefined) loc[part] = {} // initialize
        loc = loc[part] // descend
        continue
      }

      // last part, so assign the value
      loc[part] = val
    }
  }

  get(prop_str) {
    return prop_str.split('.').reduce((prev, curr) => {
      return prev ? prev[curr] : undefined
    }, this)
  }

  set relaying(val) {
    if (this.transaction) {
      this.transaction._relaying = val
    } else {
      this._relaying = val
    }
  }
  get relaying() {
    if (this.transaction && '_relaying' in this.transaction)
      return this.transaction._relaying
    return this._relaying
  }
  auth_results(message) {}
  respond(code, msg, func) {
    return func()
  }
  init_transaction(done) {
    this.transaction = new transaction.createTransaction(null, this.cfg)
    this.transaction.results = new ResultStore(this)
    if (done) done()
  }
  reset_transaction(done) {
    if (this.transaction && this.transaction.resetting === false) {
      this.transaction.resetting = true
    } else {
      this.transaction = null
    }
    if (done) done()
  }
}

exports.Connection = Connection

exports.createConnection = function (client = {}, server = {}, cfg = {}) {
  if (!cfg || Object.keys(cfg).length === 0) {
    cfg = config.get('smtp.ini', {
      booleans: [
        '+main.smtputf8',
        '+headers.add_received',
        '+headers.clean_auth_results',
      ],
    })
  }
  return new Connection(client, server, cfg)
}