prey/prey-node-client

View on GitHub
lib/agent/commands.js

Summary

Maintainability
D
2 days
Test Coverage
var common = require('./common');
var hooks = require('./hooks');
var actions = require('./actions');
var triggers = require('./triggers');
var providers = require('./providers');
var reports = require('./reports');
var updater = require('./updater');
var storage = require('./utils/storage');
var devices = require('./control-panel/api/devices');
var logger = common.logger.prefix('commands');
var watching = false; // flag for storing new commands when fired
var id;

const { v4: uuidv4 } = require('uuid');
const actions_not_allowed = ['user_activated'];
////////////////////////////////////////////////////////////////////
// helpers

// transforms this 'host:myhost.com user:god'
// into this: {host: 'myhost.com', user: 'god' }
var parse_arguments = function (args) {
  if (!args || args.trim() === '') return;
  try {
    var formatted = args
      .trim()
      .replace(/([\w\.]+)/g, '"$1"')
      .replace(/" /g, '",');
    return JSON.parse('{' + formatted + '}');
  } catch (e) {
    console.log('Invalid argument format.');
  }
};

var handle_error = function (err) {
  hooks.trigger('error', err);
};
////////////////////////////////////////////////////////////////////
// build/run/parse/perform/process exports

exports.build = function build(command, target, options) {
  var obj = { command: command, target: target };
  if (options) obj.options = options;
  return obj;
};

exports.run = function (command, target, options) {
  var obj = exports.build(command, target, options);
  if (obj) exports.perform(obj);
};

exports.parse = function (body) {
  var c;
  var matches;

  if ((matches = body.match(/^help\s?(\w+)?/))) c = ['help', matches[1]];

  // on [event] [start|stop] [something]
  if (
    (matches = body.match(
      /^(on|once) ([\w\-]+) (config|start|stop|get|set|send) (.+)/
    ))
  )
    c = ['hook', matches[1], matches[2], body];

  if ((matches = body.match(/^config read ([\w-]+)/)))
    c = ['config', [matches[1]]];

  if ((matches = body.match(/^config update (\w+)\s(?:to )?(\w+)/)))
    c = ['command', this.build('update', matches[1], matches[2])];

  if ((matches = body.match(/^upgrade/)))
    c = ['command', this.build('upgrade')];

  if ((matches = body.match(/^start ([\w\-]+)(?: (using|with) )?(.*)/)))
    c = [
      'command',
      this.build('start', matches[1], parse_arguments(matches[3])),
    ];

  if ((matches = body.match(/^watch ([\w\-]+)(?: (using|with) )?(.*)/)))
    c = [
      'command',
      this.build('watch', matches[1], parse_arguments(matches[3])),
    ];

  if ((matches = body.match(/^stop ([\w\-]+)/)))
    c = ['command', this.build('stop', matches[1])];

  if ((matches = body.match(/^unwatch ([\w\-]+)/)))
    c = ['command', this.build('unwatch', matches[1])];

  if (
    (matches = body.match(
      /^(?:get|send) ([\w\/\.]+)(?: to )?([\w@\.:\/]+)?(?: (using|with) )?(.*)/
    ))
  ) {
    // var destination = matches[2] ? [matches[1].trim(), matches[2].trim(), matches[3]] : {};
    if (matches[1][0] == '/' && matches[1].match(/\.(...?)/))
      c = ['send_file', [matches[1].trim()]];
    else if (matches[1]) c = ['command', this.build('get', matches[1].trim())];
  }

  return c;
};

exports.perform = function (command, persist = 1) {
  if (!command) return handle_error(new Error('No command received'));

  if (typeof command.options == 'string') {
    try {
      command.options = JSON.parse(command.options);
    } catch (e) {
      logger.warn('Error parsing command options: ' + e.message);
    }
  }

  // verify if id comes as part of the message.
  // if it's not present is created using uuidv4
  if (command.id) {
    id = command.id;
  } else if (command.options && command.options.messageID) {
    id = command.options.messageID;
  } else {
    id = uuidv4();
  }

  if (command.body) {
    command = command.body;
  }

  logger.info('Command received: ' + JSON.stringify(command));

  var methods = {
    start: actions.start,
    stop: actions.stop,
    watch: triggers.add,
    unwatch: actions.stop,
    get: providers.get,
    report: reports.get,
    cancel: reports.cancel,
    upgrade: updater.check,
  };

  // Intercept {command: 'get', target: 'report', options: {interval: 5}}
  // This kind of report should be storable. To ensure it can be stored we need
  // to change it to {command: 'report', target: 'stolen'}
  if (
    command.command === 'get' &&
    command.target === 'report' &&
    command.options.interval
  ) {
    command.command = 'report';
    command.target = 'stolen';
  }

  // Automation command to mark as missing and recovered from here
  if (
    command.command === 'start' &&
    (command.target === 'missing' || command.target === 'recover')
  ) {
    var set_missing = command.target === 'missing' ? true : false;

    // Set as missing or recovered on the control panel
    devices.post_missing(set_missing, (err) => {
      if (err)
        logger.warn(
          'Unable to set missing state to the device: ' + err.message
        );
    });

    // Initialize (or end) reports process on the client
    var is_stolen = reports.running().some((e) => e.name == 'stolen');
    if (set_missing && !is_stolen) {
      command.command = 'report';
      command.target = 'stolen';
    } else {
      command.command = 'cancel';
      command.target = 'stolen';
    }
  }

  var type = command.command || command.name,
    method = methods[type];

  if (method && !actions_not_allowed.find((x) => x == command.target)) {
    hooks.trigger('command', method, command.target, command.options);

    if (command.command != 'start') {
      if (
        command.command == 'get' ||
        command.command == 'report' ||
        command.command == 'cancel'
      ) {
        if (command.command == 'cancel' && command.target == 'stolen')
          delete_same_target(id, 'stolen', () => {
            logger.debug('Deleted report stored command');
          });
        method(command.target, command.options);
      } else {
        method(id, command.target, command.options);
      }
    } else {
      verify_if_executed(id, (err, executed) => {
        // Was executed and finished
        if (!executed) {
          let target = verify_if_is_full_wipe(command.target, 'fullwipe');
          command.options.target = command.target;
          command.target = target;
          method(id, command.target, command.options);
        } else {
          logger.warn(`Action with id ${id} was already executed`);
        }
      });
    }
    if (persist) update_stored(type, id, command.target, command.options);
  } else {
    handle_error(
      new Error('Unknown command: ' + (command.command || command.name))
    );
  }
};

exports.process = function (str) {
  try {
    var commands = JSON.parse(str);
    logger.info('Got commands.');
  } catch (e) {
    return handle_error(new Error('Invalid commands: ' + str));
  }

  commands.forEach(this.perform);
};

////////////////////////////////////////////////////////////////////
// command persistence
exports.store = store;

// When storing an action it gets deleted only when an action with the same name arrives.
var delete_same_target = (id, target, cb) => {
  storage.do(
    'query',
    { type: 'commands', column: 'target', data: target },
    (err, actions) => {
      if (actions && actions.length == 0) return cb();
      actions.forEach((action, index) => {
        if (id != action.id) {
          storage.do('del', { type: 'commands', id: action.id }, () => {
            if (index == actions.length - 1) return cb();
          });
        } else if (index == actions.length - 1) return cb();
      });
    }
  );
};

var store = function (type, id, name, opts, cb) {
  logger.debug('Storing command in DB: ' + [type, name].join('-'));

  delete_same_target(id, name, () => {
    if (name == 'geofencing' || name == 'triggers' || name == 'fileretrieval')
      return cb && cb();
    storage.do(
      'set',
      {
        type: 'commands',
        id: id,
        data: { command: type, target: name, options: opts },
      },
      cb
    );
  });
};

var remove = function (type, id, name, cb) {
  logger.debug('Removing command from DB: ' + [type, name].join('-'));
  storage.do('del', { type: 'commands', id: id }, cb);
};

var verify_if_executed = function (id, cb) {
  storage.do(
    'query',
    { type: 'commands', column: 'id', data: id },
    function (err, rows) {
      if (err) return cb(err);

      if ((rows && rows.length == 0) || (rows[0] && rows[0].stopped == 'NULL'))
        return cb(null, false);
      else return cb(null, true);
    }
  );
};

var verify_if_is_full_wipe = function (target, word) {
  let result = target.split(word);
  if (result && result.length == 2 && result[1] == 'windows') return word;
  return target;
};

// record when actions, triggers and reports are started
var update_stored = function (type, id, name, opts) {
  if (!watching) return;

  var storable = ['start', 'watch', 'report'];

  if (type == 'cancel')
    // report cancelled
    remove('report', id, name);
  else if (storable.indexOf(type) !== -1) {
    store(type, id, name, opts);
  }
};

// listen for new commands and add them to storage, in case the app crashes
var watch_stopped = function () {
  if (!watching) return;

  hooks.on('action', function (event, id) {
    if (event == 'stopped' || event == 'failed') {
      storage.do(
        'update',
        {
          type: 'commands',
          id: id,
          columns: 'stopped',
          values: new Date().toISOString(),
        },
        (err) => {
          if (err)
            logger.warn(
              'Unable to update stopped action timestamp for id:' + id
            );
        }
      );
    }

    if (event == 'started') {
      storage.do(
        'update',
        {
          type: 'commands',
          id: id,
          columns: 'started',
          values: new Date().toISOString(),
        },
        (err) => {
          if (err)
            logger.warn(
              'Unable to update started action timestamp for id:' + id
            );
        }
      );
    }
  });

  hooks.on('trigger', function (event, name) {
    if (event == 'stopped') remove('watch', name);
  });
};

exports.start_watching = function () {
  watching = true;
  watch_stopped();
};

exports.stop_watching = function () {
  watching = false;
};

exports.run_stored = function (cb) {
  storage.do('all', { type: 'commands' }, (err, commands) => {
    if (err || !commands) return logger.error(err.message);

    var count = Object.keys(commands).length;
    if (count <= 0) return;

    for (let id in commands) {
      if (commands[id].stopped == 'NULL') {
        logger.warn('Relaunching '); // modificar mensaje
        exports.perform(commands[id]);
        continue;
      }
    }
  });
};