haraka/haraka-plugin-log-reader

View on GitHub
index.js

Summary

Maintainability
A
1 hr
Test Coverage
'use strict';

// node.js built-in modules
const spawn = require('child_process').spawn;

let log = '/var/log/haraka.log';
let plugin;

exports.register = function () {
  plugin = this;
  this.get_logreader_ini();
  this.load_karma_ini();
}

exports.hook_init_http = function (next, server) {
  server.http.app.use('/logs/:uuid', exports.get_logs);
  server.http.app.use('/karma/rules', exports.get_rules);
  next();
}

exports.get_logreader_ini = function () {
  plugin.cfg = plugin.config.get('log.reader.ini', function () {
    plugin.get_logreader_ini();
  })

  if (plugin.cfg.log && plugin.cfg.log.file) {
    log = plugin.cfg.log.file;
  }
}

exports.load_karma_ini = function () {
  plugin.karma_cfg = plugin.config.get('karma.ini', function () {
    plugin.load_karma_ini();
  })

  if (!plugin.karma_cfg.result_awards) return;
  if (!plugin.result_awards) plugin.result_awards = {};

  Object.keys(plugin.karma_cfg.result_awards).forEach(function (anum) {
    const parts = plugin.karma_cfg.result_awards[anum]
      .replace(/\s+/, ' ')
      .split(/(?:\s*\|\s*)/);

    plugin.result_awards[anum] = {
      pi_name    : parts[0],
      property   : parts[1],
      operator   : parts[2],
      value      : parts[3],
      award      : parts[4],
      reason     : parts[5],
      resolution : parts[6],
    };
  });
}

exports.get_rules = function (req, res) {
  res.send(JSON.stringify(plugin.result_awards));
}

exports.get_logs = function (req, res) {

  const uuid = req.params.uuid;
  if (!/-/.test(uuid)) {
    return res.send('<html><body>Invalid Request</body></html>');
  }
  if (!/^[0-9A-F\-.]{12,40}$/.test(uuid)) {
    return res.send('<html><body>Invalid Request</body></html>');
  }

  // spawning a grep process is quite a lot faster than fs.read
  // (yes, I benchmarked it)
  exports.grepWithShell(log, uuid, function (err, matched) {
    if (err) return res.send(`<p>${err}</p>`);

    exports.asHtml(uuid, matched, function (html) {
      res.send(html);
    });
  });
}

exports.grepWithShell = function (file, uuid, done) {

  let matched = '';
  let searchString = uuid;

  if (/\.[0-9]{1,2}$/.test(uuid)) {
    // strip transaction off ID, so connection properties are included
    searchString = uuid.replace(/\.[0-9]{1,2}$/, '');
  }

  // var child = spawn('grep', [ '-e', regex, file ]);
  const child = spawn('grep', [ '--text', searchString, file ]);
  child.stdout.on('data', function (buffer) {
    matched += buffer.toString();
  });

  child.stdout.on('end', function (err) {
    done(err, matched);
  });
};

exports.asHtml = function (uuid, matched, done) {

  let rawLogs = ''
  let lastKarmaLine
  let monthDay = ''
  const matchMonthDay = new RegExp('^([A-Z][a-z]{2}[ ]{1,2}[0-9]{1,2}) ');

  matched.split('\n').forEach((line) => {

    if (!line) return;

    let transId;
    let replaceString = '';

    if (!monthDay) {
      try {
        [, monthDay] = matchMonthDay.exec(line)
      }
      catch (err) {
        plugin.loginfo(line)
        plugin.logerror(err)
      }
    }

    const uuidMatch = line.match(/ \[([A-F0-9\-.]{12,40})\] /);
    if (uuidMatch && uuidMatch[1]) {
      transId = uuidMatch[1].match(/\.([0-9]{1,2})$/);
    }
    if (transId && transId[1]) replaceString = `[${transId[1]}] `;

    let trimmed = line
      .replace(/\[[A-F0-9\-.]{12,40}\] /, replaceString)  // UUID
      .replace(matchMonthDay, '');                        // Mon DD

    // strip prepended hostname
    if ( / haraka\[[0-9]+\]: /.test(trimmed) ) {    // with PID
      trimmed = trimmed.replace(/(?: [a-z.-]+)? haraka\[[0-9]+\]: /, ' ');
    }
    else if ( / haraka: \[/.test(trimmed) ) {       // w/o PID
      trimmed = trimmed.replace(/(?: [a-z.-]+)? haraka: /, ' ');
    }

    rawLogs += `${trimmed}<br>`;
    if (/\[karma/.test(line) && /awards/.test(line)) {
      lastKarmaLine = line;
    }
  })

  let awardNums = [];
  if (lastKarmaLine) {
    const bits = lastKarmaLine.match(/awards: ([0-9,]+)?\s*/);
    if (bits && bits[1]) awardNums = bits[1].split(',');
  }

  done(
    `${htmlHead() +
    htmlBody(
      `for connection ${uuid} on ${monthDay}`,
      getAwards(awardNums).join(''),
      getResolutions(awardNums).join('')
    ) +
    rawLogs}</pre></div></body></html>`
  );
}

// exports.grepWithFs = function (file, regex, done) {
//     const wantsRe = new RegExp(regex);
//     const fsOpts = { flag: 'r', encoding: 'utf8' };
//     require('fs').readFile(log, fsOpts, function (err, data) {
//         if (err) throw (err);
//         let res = '';
//         data.toString().split(/\n/).forEach(function (line) {
//             if (wantsRe && !wantsRe.test(line)) return;
//             res += line + '\n';
//         });
//         done(null, res);
//     });
// };

function getAwards (awardNums) {
  if (!awardNums || awardNums.length === 0) return [];

  const awards = [];
  awardNums.forEach(function (a) {
    if (!a || !plugin.result_awards[a]) return;
    plugin.result_awards[a].id = a;
    awards.push(plugin.result_awards[a]);
  });

  const listItems = [];
  awards.sort(sortByAward).forEach(function (a) {
    const start = `<li> ${a.award},  `;
    if (a.reason) {
      listItems.push(`${start + a.reason} (${a.value})</li>`);
      return;
    }
    listItems.push(`${start + a.pi_name} ${a.property} ${a.value}</li>`);
  });
  return listItems;
}

function getResolutions (awardNums) {
  if (!awardNums || awardNums.length === 0) return [];

  const awards = [];
  awardNums.forEach(function (a) {
    if (!a || !plugin.result_awards[a]) return;
    awards.push(plugin.result_awards[a]);
  });

  const listItems = [];
  const resolutionSeen = {};
  awards.sort(sortByAward).forEach(function (a) {
    if (!a.resolution) return;
    if (resolutionSeen[a.resolution]) return;
    resolutionSeen[a.resolution] = true;
    listItems.push(`<li>${a.resolution}</li>`);
  });
  return listItems;
}

function sortByAward (a, b) {
  if (parseFloat(b.award) > parseFloat(a.award)) return -1;
  if (parseFloat(b.award) < parseFloat(a.award)) return  1;
  return 0;
}

function htmlHead () {
  return '<html> \
    <head> \
      <meta charset="utf-8"> \
      <link rel="stylesheet" href="/haraka/css/bootstrap.min.css"> \
      <link rel="stylesheet" href="/haraka/css/bootstrap-theme.min.css"> \
      <style> \
        div { padding: 1em; } \
      </style> \
    </head>';
}

function htmlBody (uuid, awards, resolve) {
  let str = '<body> \
        <div class="tab-content"> \
        <h3>Sorry if we blocked your message:</h3> \
        <p>Our filters mistook your server for a malicious computer attempting \
        to send spam. To improve your mail servers reputation, please contact \
        your IT helpdesk or Systems Administrator and ask them for help.</p>';

  if (awards) {
    str += `<hr><h3>Policy Rules Matched</h3> \
        <ul>${awards}</ul>`;
  }

  if (resolve) {
    str += `<hr><h3>Steps to Resolve</h3> \
        <ul>${resolve}</ul>`;
  }

  str += `<hr> \
        <h3>Raw Logs</h3> \
        <p>${uuid}</p> \
        <pre> \
        \n`;
  return str;
}