KeepOut

Node.js, React, Redux, WebSockets
Close X

KeepOut is an ongoing personal project with the only purpose of experimenting and familiarizing with cutting-edge technologies and explore little-known areas.

It’s an antivirus app for MacOS backed by ClamAV®, an open-source CLI antivirus engine for detecting trojans, viruses, malware & other malicious threats.

The client is written in ES6, built on React with a Redux architecture and the API is written in Node.js with the use of ExpressJS framework.
The API is the middleware between the front-end and the ClamAV® binaries (which gets installed on startup using the MacOS pre-installed package manager Homebrew).

WebSockets and streams are used throughout the application for streaming the terminal’s output to the user such as real-time logs and process of definitions update.

The final release will be wrapped inside an Electron shell for the users to download and use as a native iOS application.

Designed in Sketch (I’ve finally switched over from Adobe Photoshop), the app comes with a minimalistic UI and a three-colours palette.

Bootstrap 4 for the CSS.

Some screenshots and code snippets as of date 12th of July 2017:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';

import * as virusDefinitionsActions from '../actions/virusDefinitionsActions';
import * as logsActions from '../actions/logsActions';
import Sync from '../components/Sync';
import './VirusDefinitions.css';

class VirusDefinitions extends Component {
  constructor(props) {
    super(props);
    this.state = { syncing: false }
    this.delay = 1000;
  }

  componentWillMount() {
    this.props.getLastUpdate();
  }

  componentWillUnmount() {
    clearInterval(this.interval);
    clearTimeout(this.timeout);
  }

  componentWillReceiveProps(nextProps, nextState) {
    if (nextProps.virusDefinitions.lastUpdate) {
      clearInterval(this.interval);
      this.timeFromLastUpdate(nextProps.virusDefinitions.lastUpdate);
      this.interval = setInterval(() => {
        this.timeFromLastUpdate(nextProps.virusDefinitions.lastUpdate);
      }, this.delay);
    }

    if (this.state.syncing) {
      this.timeout = setTimeout(() => {
          this.setState({ syncing: false });
          this.props.setLastUpdate(moment().format());
      }, this.delay);
    }
  }

  startUpdate(event) {
    if (!this.state.syncing) {
      this.setState({ syncing: true });
      this.props.updateDefinitions();
    }
  }

  timeFromLastUpdate(lastUpdate) {
    this.setState({ timeElapsed: moment(lastUpdate).fromNow() })
  }

  render() {
    return (
      <footer className="VirusDefinitions mt-auto">
        <div className="d-flex justify-content-between align-items-center">
          <div>
             { this.startUpdate() }} className="h5" role="button">Update virus definitions
            <div className="text-primary">Last update: {this.state.timeElapsed || 'Never'}</div>
          </div>
          {this.state.syncing && <Sync/>}
        </div>
      </footer>
    );
  }
}

const mapStateToProps = ({ virusDefinitions }) => {
  return {
    virusDefinitions
  }
}

export default connect(mapStateToProps, { ...virusDefinitionsActions, ...logsActions })(VirusDefinitions);

 

import axios from 'axios';

import { UPDATE_DEFINITIONS, GET_LAST_UPDATE, SET_LAST_UPDATE, CLEAR_LOGS, DISPLAY_ERROR } from './types';
import { API_ROOT } from './const';
import { clearLogsRequest } from './logsActions';

export const updateDefinitions = () => {
  return (dispatch) => {
    clearLogsRequest('logs-definitions')
      .then((response) => {
        dispatch({
          type: CLEAR_LOGS,
          payload: response
        });

        axios.get(`${API_ROOT}/update-definitions`)
          .then((response) => {
            dispatch({
              type: UPDATE_DEFINITIONS,
              payload: response
            });
        })
        .catch((e) => {
          dispatch({
            type: DISPLAY_ERROR,
            payload: e
          });
        });
    })
    .catch((e) => {
      dispatch({
        type: DISPLAY_ERROR,
        payload: e
      });
    });
  }
}

export const getLastUpdate = () => {
  const lastUpdate = localStorage.getItem('lastUpdate');
  return {
    type: GET_LAST_UPDATE,
    payload: lastUpdate
  }
}

export const setLastUpdate = (date) => {
  localStorage.setItem('lastUpdate', date);
  return {
    type: SET_LAST_UPDATE,
    payload: date
  }
}

 

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const http = require('http');
const WebSocket = require('ws');

const routes = require('./routes');
const Logs = require('./controllers/Logs');

const port = process.env.PORT || 3090;
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.use(cors());
app.use(bodyParser.json({ type: '*/*' }));
app.use(routes);

wss.on('connection', (ws, req) => { new Logs().readLogsStream(ws, req); });

app.listen(port, () => {
  console.log('Server listening on:', port); // eslint-disable-line no-console
});

server.listen(4090, () => {
  console.log('WebSockets listening on:', server.address().port); // eslint-disable-line no-console
});

 

const { spawn } = require('child_process');
const Logs = require('./Logs');
const config = require('../config');

class UpdateDefinitions {
  constructor() {
    this.update = this.update.bind(this);
    this.logs = new Logs();
  }

  startUpdate(res) {
    const startUpdate = spawn('freshclam', ['-v']);
    startUpdate.stdout.on('data', (data) => { this.onData(res, data); });
    startUpdate.on('close', (code) => { this.onClose(res, code); });
    startUpdate.on('error', (err) => { this.onError(res, err); });
  }

  onData(res, data) {
    this.logs.write(data);
  }

  onClose(res) {
    this.logs.end();
    if (res.headersSent) {
      res.end();
    } else {
      res.send({ updated: true });
    }
  }

  onError(res, err) {
    res.status(500).send(err);
  }

  update(req, res) {
    this.logs.writeLogsStream(`${config.rootDir}/${config.logsDirectory}/logs-definitions.log`, () => { this.startUpdate(res); });
  }
}

module.exports = UpdateDefinitions;