diff --git a/Dockerfile b/Dockerfile index efe6460..1c66662 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,3 +27,9 @@ RUN sudo chmod +x ${JAMOTA_ROOT}/start.sh # redis script setup COPY ./redis/redis.sh ${JAMOTA_ROOT}/redis/redis.sh RUN sudo chmod +x ${JAMOTA_ROOT}/redis/redis.sh + +RUN sudo mkdir -p ${JAMOTA_ROOT}/channels/files +RUN sudo chmod a+rwx ${JAMOTA_ROOT}/channels/files + +RUN sudo mkdir -p ${JAMOTA_ROOT}/channels/commands +RUN sudo chmod a+rwx ${JAMOTA_ROOT}/channels/commands diff --git a/TODO.md b/TODO.md index a77d870..3741471 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,7 @@ * through website, user executes jamkill to stop current running JAMScript program * through website, user uploads new .jxe file * through website, user executes jamrun to start new file + * update jxe_loader to match website data 1. website * robust UI * auto-refresh page data (pull/push) diff --git a/ota-portal/app.js b/ota-portal/app.js index f35dc7f..3b8931c 100644 --- a/ota-portal/app.js +++ b/ota-portal/app.js @@ -17,6 +17,7 @@ var app = express(); // basic middleware app.use(logger('dev')); +app.use(express.raw()); app.use(express.text({ type: "text/*" })); diff --git a/ota-portal/bin/www b/ota-portal/bin/www index ceb9c96..cde5308 100644 --- a/ota-portal/bin/www +++ b/ota-portal/bin/www @@ -10,6 +10,9 @@ var fs = require('fs'); var crypto = require('crypto'); var rclient = require('../utils/redis-client'); +// Print current user +console.log(require("os").userInfo().username); + /** * Create admin user. */ diff --git a/ota-portal/public/javascripts/nodes/tools.js b/ota-portal/public/javascripts/nodes/tools.js index de8ec90..c790da4 100644 --- a/ota-portal/public/javascripts/nodes/tools.js +++ b/ota-portal/public/javascripts/nodes/tools.js @@ -10,7 +10,7 @@ async function deleteNode(nodeId) { await dataRequest("DELETE", `nodes/${nodeId}`); } -async function uploadFile(networkId) { +async function getChannel(networkId) { // get node IDs to upload to let nodeSelectElements = document.querySelectorAll("input.node-selector"); let nodeIds = []; @@ -24,15 +24,18 @@ async function uploadFile(networkId) { let channelRes = await dataRequest("POST", `networks/${networkId}/channel`, undefined, { "nodeIds": nodeIds }); + console.log(channelRes); +} - return; +async function uploadFile(networkId) { + await getChannel(networkId); // upload file let file = document.querySelector("#file-input").files[0]; const reader = new FileReader(); reader.onload = (e) => { console.log("Sending file " + file.name); - dataRequest("POST", "file", undefined, e.target.result, "application/octet-stream") + dataRequest("POST", `networks/${networkId}/channel/file`, undefined, e.target.result, "application/octet-stream") .then((data) => { console.log(data); }) @@ -40,5 +43,18 @@ async function uploadFile(networkId) { console.log(err); }); }; + + // read file reader.readAsArrayBuffer(file); } + +async function uploadCommand(networkId) { + await getChannel(networkId); + + // read command + const cmd = document.querySelector("#cmd-input").value; + + // upload command + let cmdRes = await dataRequest("POST", `networks/${networkId}/channel/cmd`, undefined, cmd, "text/plain"); + console.log(cmdRes); +} diff --git a/ota-portal/routes/networkChannels.js b/ota-portal/routes/networkChannels.js index fad9ec1..e051ec7 100644 --- a/ota-portal/routes/networkChannels.js +++ b/ota-portal/routes/networkChannels.js @@ -12,6 +12,31 @@ const rclient = require("../utils/redis-client"); const network = require("../utils/network"); const channel = require("../utils/channel"); const node = require("../utils/node"); +const fs = require("fs"); + +const { spawn } = require("child_process"); + +const scriptPath = (script) => `${process.env.JAMOTA_ROOT}/ota-portal/bin/${script}.js`; + +async function execNodeScript(args, onstdout, onstderr) { + return new Promise((resolve, reject) => { + const proc = spawn("node", args, { + detached: true + }); + + if (onstdout) { + proc.stdout.on("data", onstdout); + } + + if (onstderr) { + proc.stderr.on("data", onstderr); + } + + proc.on("exit", (code) => { + resolve(code); + }); + }); +} /** * Create a channel to communicate with multiple nodes. Each network can only have one active channel. @@ -45,7 +70,51 @@ router.post("/:id/channel", errors.asyncWrap(async function(req, res) { await channel.newChannelObj(networkId, nodeIds); // return response - res.status(200); + res.sendStatus(200); })); +/** + * Upload a file to the channel. + */ +router.post("/:id/channel/file", async function(req, res) { + req.setEncoding(null); + + try { + // persist file locally + fs.writeFileSync(`${process.env.JAMOTA_ROOT}/channels/${req.params.id}`, Buffer.from(req.body)); + + // asynchronously dispatch process to upload file to nodes + execNodeScript([scriptPath("jxe_loader"), "--networkId", req.params.id, "--type", "file"], (data) => { + console.log(data.toString()); + }); + + res.sendStatus(200); + } + catch (e) { + console.log("Failed", e); + res.sendStatus(500); + } +}); + +/** + * Upload a command to the channel. + */ +router.post("/:id/channel/cmd", async function(req, res) { + try { + let cmd = req.body; + console.log(cmd); + + // dispatch process to send command to nodes + execScript([scriptPath("jxe_loader"), "--networkId", req.params.id, "--type", "command"], (data) => { + console.log(data.toString()); + }); + + res.sendStatus(200); + } + catch (e) { + console.log("Failed", e); + res.sendStatus(500); + } +}); + module.exports = router; diff --git a/ota-portal/routes/networks.js b/ota-portal/routes/networks.js index fc67d72..4db7bfe 100644 --- a/ota-portal/routes/networks.js +++ b/ota-portal/routes/networks.js @@ -11,6 +11,7 @@ const request = require("../utils/request"); const ijam_types = require("../utils/ijam_types"); const network = require("../utils/network"); +const passphrases = require("../utils/network_passphrase"); const node = require("../utils/node"); /** @@ -61,7 +62,7 @@ router.delete("/:id", errors.asyncWrap(async function(req, res, next) { await rclient.del(node.networkNodesKey(networkId)); // delete passphrases - await network.clearPassphrases(networkId); + await passphrases.clearPassphrases(networkId); // remove from user network list await rclient.removeFromSet(network.userNetworksKeyFromReq(req), networkId); @@ -91,7 +92,7 @@ async function filterNetworkEntries(networkIds) { networks[networkId] = map(networkObj, await rclient.getSetSize(node.networkNodesKey(networkId)), - await network.getNumberOfPassphrases(networkId)); + await passphrases.getNumberOfPassphrases(networkId)); } } diff --git a/ota-portal/utils/network_passphrase.js b/ota-portal/utils/network_passphrase.js index f1079cd..b07e1d0 100644 --- a/ota-portal/utils/network_passphrase.js +++ b/ota-portal/utils/network_passphrase.js @@ -1,4 +1,6 @@ +const rclient = require("./redis-client"); + const maxPassphraseLength = 16; /** Get the key mapping to the network's active passphrases. */ diff --git a/ota-portal/views/node/list.pug b/ota-portal/views/node/list.pug index ebbe418..a7aeaae 100644 --- a/ota-portal/views/node/list.pug +++ b/ota-portal/views/node/list.pug @@ -6,9 +6,15 @@ block content h2 Command center div - label Select file to upload to selected nodes - input(type="file" id="file-input") - input(type="submit" value="Upload", onclick="uploadFile('" + networkId + "')") + p + label Select file to upload to selected nodes + input(type="file" id="file-input") + input(type="submit" value="Upload", onclick="uploadFile('" + networkId + "')") + + p + label Send command to selected nodes + input(type="text" id="cmd-input") + input(type="submit" value="Upload" onclick="uploadCommand('" + networkId + "')") h2 Table of nodes