3
\$\begingroup\$

The goal of my project is to build a mini "anonymous" network (like Tor is) (command-line only) using Node.js and frameworks (socket.io / axios / express). A network where I can download files (HTML etc) anonymously. I have central server and nodes (clients). At start each client connects to central server (using socket.io) and server appends it to the list hosts. Each minute client asks server for latest list of alive nodes.

I came here because I did my best and I cannot see more bugs / errors / flaws. Also, I am not good at security and only on my way to learning deep.

I have some questions and I will be grateful if you advise how to improve my code:

  1. What can be improved and how?
  2. What about security?
  3. My node-chain isn't that good now. How I can make it longer (right now I have to set 1 minute timer and start nodes at different time but it is bad; in- fact that if some node will die in the middle, my code would be ruined, so I am looking for real-time long (at least 3 nodes) chain)
  4. Right now I am receiving the answer as HTML in my console. Is it possible to do so that if node localhost:4500 sends request I can see the answer as website at localhost:4500?

Maybe my central server should create graf of clients where each client (node) will have 3 (strict) neighbor nodes, but how to implement that? (image visualisation below!)

Clients graf

My code is fully working and you can see it below with some rules that my project has.


URL (file) download request

  1. If the node initiates this query itself, it must keep in mind the id to recognize the answer later

  2. A network node initiating request sends a message to all known neighbors

  3. For the first time when receiving this message, the network node decides whether to download the file (probability x%, mine is 50% right now)

    1. If the file is not downloaded, the network node sends the message to all its neighbors, unless the neighbor IP matches the IP address of the transmitting node of the message

    2. When the file is downloaded, a file type message is initiated

    3. In both cases, the message id and the forwarding network node IP must be memorized

  4. Receiving a second message with the same id (or self-initiated) will be ignored

  5. The node initiating the request must take into consideration the possibility that the response is not guaranteed due to the unstable nature of the network

Return the contents of the file

  1. The network node initiating the request sends a message to the neighbor from whom they downloaded the same message with the same id

  2. If the previous step fails, the message will be sent to all neighbors

  3. For the first time when receiving this message, the network node decides if the file was its own (id parameter)

    1. If not, the network node sends the message to a neighbor who received the download request with the same id

    2. If the previous step failed (or no such neighbor is reported), the message will be sent to all neighbors, except if the neighbor IP matches the sender of the file in question with the IP address

    3. If the file was requested by the node, the query body is parsed from the status and, if any, the contents of the file

  4. Receiving a second message with the same id (or self-initiated) will be ignored


install socket.io, express, axios, request, body-parser, event-emitter

To test I am using POSTMAN and here is request example:

localhost:4500/download?id=763145322224615&url=https://google.com&port=4500

I am sending download request to node with port 4500, id is a random number which will be memorized by this node to recognize the answer later. url is file to be downloaded.

Client (at least two clients to get it work!)

node client.js 4500
node client.js 4501

Server

node server.js

Server

"use strict";

const app = require("express")();
const http = require("http").Server(app);
const io = require("socket.io")(http);

var hosts = [];

app.get("/hosts", function(req, res) {
  res.send(hosts);
});

http.listen(3000, function() {
  console.log("listening on *:3000");
});

io.on("connection", function(socket) {
  const address = socket.handshake.address;
  const ipAddress = address.substring(address.lastIndexOf(':') + 1, address.length);
  const port = socket.handshake.query.port;
  hosts.push({ip: ipAddress, port: port});

  console.log(`A machine with ${ipAddress}:${port} successfully connected!`);
  socket.join("hosts");
  console.log(`A machine successfully joined!`);
  socket.emit("hosts", hosts);
  // socket.broadcast.to("hosts").emit("hosts", hosts);

  socket.on("disconnect", function() {
    console.log(`A machine with ${ipAddress}:${port} successfully disconnected!`);
    hosts = hosts.filter(item => item.ip !== ipAddress || item.port !== port);
    // socket.broadcast.to("hosts").emit("hosts", hosts);
  });
});

Client

"use strict";

const io = require('socket.io-client');
const app = require('express')();
const http = require('http')
const server = http.Server(app);
const request = require('request');
const axios = require('axios');
const bodyParser = require('body-parser')
const EventEmitter = require('events');

class MyEmitter extends EventEmitter {};
const event = new MyEmitter();

app.use(bodyParser.urlencoded({ extended: true}));
app.use(bodyParser.json())

const states = {"REQUEST": 1, "SEND": 2, "DOWNLOAD": 3, "FINISH": 4};
const errorCodes = {"NOT_ACCEPTABLE": "NOT ACCEPTABLE", "OK": "OK"};
const args = process.argv;

const myIpAddress = "127.0.0.1"
const myPort = args[2] || 4500;

const serverAddress = "127.0.0.1";
const serverPort = 3000;
const socket = io(`http://${serverAddress}:${serverPort}/`, {
  query: `port=${myPort}`
});

const laziness = 0.5;

var hosts = [];
var requests = {};

server.listen(myPort, myIpAddress, () => {
  console.log(`Listening ${myIpAddress}:${myPort}`);
});

socket.on("hosts", data => {
  //console.log("Received data: ", data)
  hosts = data;
});

app.get("/download", (req, res) => {
  const id = req.query.id;
  const url = req.query.url;
  const ip = req.connection.remoteAddress;
  const port = req.query.port;

  if (id == null || url == null || ip == null || port == null) {
    res.sendStatus(404);
    return;
  }
  if (requests[id] != null) {
    res.status(200).send(errorCodes.NOT_ACCEPTABLE);
    return;
  }
  console.log(`Downloading URL: ${url} with ID: ${id}`);

  requests[id] = {
    ip: ip,
    port: port,
    url: url,
    host: false,
    state: states.REQUEST
  }

  event.emit('check', id);
  res.sendStatus(200);
});

app.get('/check', (req, res) => {
  let id = req.query.id;

  res.sendStatus(requests[id] != null && requests[id].state != states.REQUEST ? 200 : 204);
});

app.post('/file', (req, res) => {
  const id = req.query.id;
  console.log('File request');

  if (requests[id] == null || requests[id].state === states.FINISH) {
    res.status(200).send(errorCodes.NOT_ACCEPTABLE);
    return;
  }

  if (requests[id].host) {
    requests[id].state = states.FINISH;
    console.log('StatusCode: ', req.body.status);
    console.log('Mime-type: ', req.body['mime-type']);
    console.log("Content: ", Buffer.from(req.body.content, "base64").toString("ascii"));
  } else {
    console.log('Sending back to host');
    event.emit("sendBackToHost", id, req.body);
  }

  res.sendStatus(200);
});

event.on('check', (id) => {
  axios.get(`http://${requests[id].ip}:${requests[id].port}/check?id=${id}`)
    .then(res => {
      if (res.status === 204) {
        requests[id].url = encodeUrl(requests[id].url);
        requests[id].host = true;
        event.emit('sendNext', id);
      } else if (res.status === 200)
        event.emit('downloadOrSend', id);
    })
    .catch(err => {
      console.error('Error with checking', err);
    })
});

event.on('downloadOrSend', (id) => {
  if (isDownloadState()) {
    axios.get(decodeUrl(requests[id].url))
      .then(res => {
        const body = {
          content: Buffer.from(res.data).toString('base64'),
          status: res.status,
          "mime-type": res.headers["content-type"]
        };

        requests[id].state = states.DOWNLOAD;
        event.emit('sendBackToHost', id, body)
      })
      .catch(err => {
        console.error('Error while downloading file', err.response);
      })
    return;
  }
  event.emit('sendNext', id);
})

event.on('sendBackToHost', (id, body) => {
  axios.post(`http://${requests[id].ip}:${requests[id].port}/file?id=${id}`, body)
    .catch(err => {
      console.error("Error while sending back to host", err.response);
      event.emit('sendFileBackToAll', id, body);
    });
});

event.on('sendNext', (id) => {
  requests[id].state = states.SEND;
  for (let h of hosts) {
    if (h.ip == requests[id].ip && h.port == requests[id].port) continue;
    if (h.ip == myIpAddress && h.port == myPort) continue;

    axios.get(`http://${h.ip}:${h.port}/download?id=${id}&url=${requests[id].url}&port=${myPort}`)
      .catch(err => {
        console.error('Error while sending requests to neighbours', err.response);
      });
  }
});

event.on('sendFileBackToAll', (id, body) => {
  for (let h of hosts) {
    if (h.ip == myIpAddress && h.port == myPort) continue;

    axios.post(`http://${h.ip}:${h.port}/file?id=${id}`, body)
      .catch(err => {
        console.error("Error while sending back to all hosts", err.response);
      });
  }
});

function isDownloadState() {
  return Math.random() >= laziness;
}

function encodeUrl(url) {
  return (decodeURIComponent(url) === url) ? encodeURIComponent(url) : url;
}

function decodeUrl(url) {
  return (decodeURIComponent(url) === url) ? url : decodeURIComponent(url);
}

setInterval(() => {
  axios.get(`http://${serverAddress}:${serverPort}/hosts`)
    .then(res => {
      hosts = res.data;
    })
    .catch(err => {
      console.error("Error while fetching data from server", err.response);
    })
}, 60000)
\$\endgroup\$
1
\$\begingroup\$

I don't know javascript but some things got me wondering. I apologize in advance if I overlooked the obvious. Most of the things I point out here you can read about on the OWASP site. It's a great resource.

  • I couldn't tell if you are validating input, this is definitely a good idea. For example the urls that are being passed.
  • You should not be accepting raw HTML, this would be good to sanitize first.
  • The central server maintains a list of known hosts, but how do the clients connecting to the central server know they are not talking to a 'malicious' one? You can counter this by using public/private-keys(take a look at libsodium), so you could for example have the central server sign the hosts file\list. Then have the central hosts public key hard-coded into the clients, so that they can verify the file is 'trusted'.
  • All data is transmitted in the cleartext as I can tell, so an obvious improvement would be expanding on the public-key approach and have all nodes generate a keypair, where the public keys belonging to each node, are listed in the hosts file/list. Then whenever a node sends data to another node, that data can also be encrypted (This is much like an approach of TOR if I remember correctly).
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.