Convert nbi.coffee to JS

This commit is contained in:
Zaid Abdulla committed 2018-08-21 21:01:19 -07:00
1 parent 91fb6c1c5a
commit c836c73ffe
3 files changed
+648 -517

No files matched your search

+648
View File
@@ -0,0 +1,648 @@
/**
* Copyright 2013-2018 Zaid Abdulla
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const url = require("url");
const mongodb = require("mongodb");
const querystring = require("querystring");
const vm = require("vm");
const childProcess = require("child_process");
const config = require("./config");
const db = require("./db");
const query = require("./query");
const apiFunctions = require("./api-functions");
const cache = require("./cache");
const VERSION = require("../package.json").version;
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tasks\/?$/;
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-\_\%]+)(\/[a-zA-Z_]*)?$/;
const TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tags\/([a-zA-Z0-9\-\_\%]+)\/?$/;
const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-\_\%]+)\/?$/;
const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-\_\%]+)\/?$/;
const FILES_REGEX = /^\/files\/([a-zA-Z0-9\%\!\*\'\(\)\;\:\@\&\=\+\$\,\?\#\[\]\-\_\.\~]+)\/?$/;
const PING_REGEX = /^\/ping\/([a-zA-Z0-9\-\_\.\:]+)\/?$/;
const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/;
const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/;
const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-\_\%]+)\/?$/;
const VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-\_\%]+)\/?$/;
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-\_\%\:]+)\/?$/;
function throwError(err, httpResponse) {
if (httpResponse) {
httpResponse.writeHead(500, {"Connection": "close"});
httpResponse.end(`${err.name}: ${err.message}`);
}
throw err;
}
function listener(request, response) {
const chunks = [];
let bytes = 0;
response.setHeader("GenieACS-Version", VERSION);
request.addListener("data", (chunk) => {
chunks.push(chunk);
bytes += chunk.length;
});
function getBody() {
// Write all chunks into a Buffer
const body = new Buffer(bytes);
let offset = 0;
for (const chunk of chunks) {
chunk.copy(body, offset, 0, chunk.length);
offset += chunk.length;
}
return body;
}
request.addListener("end", () => {
const body = getBody();
const urlParts = url.parse(request.url, true);
if (PRESETS_REGEX.test(urlParts.pathname)) {
const presetName = querystring.unescape(PRESETS_REGEX.exec(urlParts.pathname)[1]);
if (request.method === "PUT") {
const preset = JSON.parse(body);
preset._id = presetName;
db.presetsCollection.save(preset, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else if (request.method === "DELETE") {
db.presetsCollection.remove({"_id": presetName}, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else {
response.writeHead(405, {"Allow": "PUT, DELETE"});
response.end("405 Method Not Allowed");
}
} else if (OBJECTS_REGEX.test(urlParts.pathname)) {
const objectName = querystring.unescape(OBJECTS_REGEX.exec(urlParts.pathname)[1]);
if (request.method === "PUT") {
const object = JSON.parse(body);
object._id = objectName;
db.objectsCollection.save(object, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else if (request.method === "DELETE") {
db.objectsCollection.remove({"_id": objectName}, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else {
response.writeHead(405, {"Allow": "PUT, DELETE"});
response.end("405 Method Not Allowed");
}
} else if (PROVISIONS_REGEX.test(urlParts.pathname)) {
const provisionName = querystring.unescape(PROVISIONS_REGEX.exec(urlParts.pathname)[1]);
if (request.method === "PUT") {
const object = {
_id: provisionName,
script: body.toString()
};
try {
new vm.Script(`"use strict";(function(){\n${object.script}\n})();`);
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
db.provisionsCollection.save(object, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else if (request.method === "DELETE") {
db.provisionsCollection.remove({"_id": provisionName}, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else {
response.writeHead(405, {"Allow": "PUT, DELETE"});
response.end("405 Method Not Allowed");
}
} else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) {
const virtualParameterName = querystring.unescape(VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1]);
if (request.method === "PUT") {
const object = {
_id: virtualParameterName,
script: body.toString()
};
try {
new vm.Script(`"use strict";(function(){\n${object.script}\n})();`);
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
db.virtualParametersCollection.save(object, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else if (request.method === "DELETE") {
db.virtualParametersCollection.remove({"_id": virtualParameterName}, (err) => {
if (err)
return throwError(err, response);
cache.del("presets_hash", (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else {
response.writeHead(405, {"Allow": "PUT, DELETE"});
response.end("405 Method Not Allowed");
}
} else if (TAGS_REGEX.test(urlParts.pathname)) {
const r = TAGS_REGEX.exec(urlParts.pathname);
const deviceId = querystring.unescape(r[1]);
const tag = querystring.unescape(r[2]);
if (request.method === "POST") {
db.devicesCollection.update(
{"_id": deviceId},
{"$addToSet": {"_tags": tag}},
{safe: true},
(err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
}
);
} else if (request.method === "DELETE") {
db.devicesCollection.update(
{"_id": deviceId},
{"$pull": {"_tags": tag}},
{safe: true},
(err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
}
);
} else {
response.writeHead(405, {"Allow": "POST, DELETE"});
response.end("405 Method Not Allowed");
}
} else if (FAULTS_REGEX.test(urlParts.pathname)) {
if (request.method === "DELETE") {
const faultId = querystring.unescape(FAULTS_REGEX.exec(urlParts.pathname)[1]);
const deviceId = faultId.split(":", 1)[0];
const channel = faultId.slice(deviceId.length + 1);
db.faultsCollection.remove({_id: faultId}, (err) => {
if (err)
return throwError(err, response);
if (channel.startsWith("task_")) {
const objId = new mongodb.ObjectID(channel.slice(5));
return db.tasksCollection.remove({_id: objId}, (err) => {
if (err)
return throwError(err, response);
cache.del(`${deviceId}_tasks_faults_operations`, (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
}
cache.del(`${deviceId}_tasks_faults_operations`, (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
} else {
response.writeHead(405, {"Allow": "DELETE"});
response.end("405 Method Not Allowed");
}
} else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) {
if (request.method === "POST") {
const deviceId = querystring.unescape(DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]);
if (body.length) {
const task = JSON.parse(body);
task.device = deviceId;
apiFunctions.insertTasks(task, (err) => {
if (err)
return throwError(err, response);
cache.del(`${deviceId}_tasks_faults_operations`, (err) => {
if (err)
return throwError(err, response);
if (urlParts.query.connection_request != null) {
apiFunctions.connectionRequest(deviceId, (err) => {
if (err) {
response.writeHead(202, err.message, {"Content-Type": "application/json"});
response.end(JSON.stringify(task));
return;
}
const DEVICE_ONLINE_THRESHOLD = config.get("DEVICE_ONLINE_THRESHOLD", deviceId);
apiFunctions.watchTask(deviceId, task._id, DEVICE_ONLINE_THRESHOLD, (err, status) => {
if (err)
return throwError(err, response);
if (status === "timeout") {
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json"
});
response.end(JSON.stringify(task));
} else if (status === "fault") {
db.tasksCollection.findOne({_id: task._id}, (err, task) => {
if (err)
return throwError(err, response);
response.writeHead(202, "Task faulted", {
"Content-Type": "application/json"
});
response.end(JSON.stringify(task));
});
} else {
response.writeHead(200, {
"Content-Type": "application/json"
});
response.end(JSON.stringify(task));
}
});
});
} else {
response.writeHead(202, {
"Content-Type": "application/json"
});
response.end(JSON.stringify(task));
}
});
});
} else if (urlParts.query.connection_request != null) {
// No task, send connection request only
apiFunctions.connectionRequest(deviceId, (err) => {
if (err) {
response.writeHead(504);
response.end(`${err.name}: ${err.message}`);
return;
}
response.writeHead(200);
response.end();
});
} else {
response.writeHead(400);
response.end();
}
} else {
response.writeHead(405, {"Allow": "POST"});
response.end("405 Method Not Allowed");
}
} else if (TASKS_REGEX.test(urlParts.pathname)) {
const r = TASKS_REGEX.exec(urlParts.pathname);
const taskId = querystring.unescape(r[1]);
const action = r[2];
if (!action || action === "/") {
if (request.method === "DELETE") {
db.tasksCollection.findOne(
{"_id": new mongodb.ObjectID(taskId)},
{"device": 1}, (err, task) => {
if (err)
return throwError(err, response);
if (!task) {
response.writeHead(404);
response.end("Task not found");
return;
}
const deviceId = task.device;
db.tasksCollection.remove(
{"_id": new mongodb.ObjectID(taskId)}, (err) => {
if (err)
return throwError(err, response);
db.faultsCollection.remove({_id: `${deviceId}:task_${taskId}`}, (err) => {
if (err)
return throwError(err, response);
cache.del(`${deviceId}_tasks_faults_operations`, (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
});
});
} else {
response.writeHead(405, {"Allow": "PUT DELETE"});
response.end("405 Method Not Allowed");
}
} else if (action === "/retry") {
if (request.method === "POST") {
db.tasksCollection.findOne({"_id": new mongodb.ObjectID(taskId)}, {"device": 1}, (err, task) => {
if (err)
return throwError(err, response);
const deviceId = task.device;
db.faultsCollection.remove({_id: `${deviceId}:task_${taskId}`}, (err) => {
if (err)
return throwError(err, response);
cache.del(`${deviceId}_tasks_faults_operations`, (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
});
});
} else {
response.writeHead(405, {"Allow": "POST"});
response.end("405 Method Not Allowed");
}
} else {
response.writeHead(404);
response.end();
}
} else if (FILES_REGEX.test(urlParts.pathname)) {
const filename = querystring.unescape(FILES_REGEX.exec(urlParts.pathname)[1]);
if (request.method === "PUT") {
const metadata = {
fileType: request.headers.filetype,
oui: request.headers.oui,
productClass: request.headers.productclass,
version: request.headers.version
};
const gs = new mongodb.GridStore(db.mongoDb, filename, filename, "w", {
metadata: metadata
});
gs.open((err, gs) => {
if (err)
return throwError(err, response);
gs.write(body, (err) => {
if (err)
return throwError(err, response);
gs.close((err) => {
if (err)
return throwError(err, response);
response.writeHead(201);
response.end();
});
});
});
} else if (request.method === "DELETE") {
mongodb.GridStore.unlink(db.mongoDb, filename, (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
} else {
response.writeHead(405, {"Allow": "PUT, DELETE"});
response.end("405 Method Not Allowed");
}
} else if (PING_REGEX.test(urlParts.pathname)) {
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
childProcess.exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
if (err) {
response.writeHead(404, {"Cache-Control": "no-cache"});
response.end(`${err.name}: ${err.message}`);
return;
}
response.writeHead(200, {
"Content-Type": "text/plain",
"Cache-Control": "no-cache"
});
response.end(stdout);
});
} else if (DELETE_DEVICE_REGEX.test(urlParts.pathname)) {
if (request.method !== "DELETE") {
response.writeHead(405, {"Allow": "DELETE"});
response.end("405 Method Not Allowed");
return;
}
const deviceId = querystring.unescape(DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1]);
apiFunctions.deleteDevice(deviceId, (err) => {
if (err)
return throwError(err, response);
response.writeHead(200);
response.end();
});
} else if (QUERY_REGEX.test(urlParts.pathname)) {
let collectionName = QUERY_REGEX.exec(urlParts.pathname)[1];
// Convert to camel case
let i = collectionName.indexOf("_");
while (i++ >= 0) {
const up = i < collectionName.length ? collectionName[i].toUpperCase() : "";
collectionName = collectionName.slice(0, i - 1) + up + collectionName.slice(i + 1);
i = collectionName.indexOf("_", i);
}
if (request.method !== "GET" && request.method !== "HEAD") {
response.writeHead(405, {"Allow": "GET, HEAD"});
response.end("405 Method Not Allowed");
return;
}
const collection = db[`${collectionName}Collection`];
if (!collection) {
response.writeHead(404);
response.end("404 Not Found");
return;
}
let q = {};
if (urlParts.query.query) {
try {
q = JSON.parse(urlParts.query.query);
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
}
switch (collectionName) {
case "devices":
q = query.expand(q);
break;
case "tasks":
q = query.sanitizeQueryTypes(q, {
_id: (v) => new mongodb.ObjectID(v),
timestamp: (v) => new Date(v),
retries: Number
});
break;
case "faults":
q = query.sanitizeQueryTypes(q, {
timestamp: (v) => new Date(v),
retries: Number
});
}
let projection = null;
if (urlParts.query.projection) {
projection = {};
for (let p of urlParts.query.projection.split(","))
projection[p.trim()] = 1;
}
const cur = collection.find(q, projection, {batchSize: 50});
if (urlParts.query.sort) {
const s = JSON.parse(urlParts.query.sort);
const sort = {};
for (let [k, v] of Object.entries(s)) {
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
sort[`${k}._value`] = v;
else
sort[k] = v;
}
cur.sort(sort);
}
if (urlParts.query.skip)
cur.skip(parseInt(urlParts.query.skip));
let limit;
if (urlParts.query.limit)
cur.limit(limit = parseInt(urlParts.query.limit));
cur.count(false, (err, total) => {
if (err)
return throwError(err);
response.writeHead(200, {
"Content-Type": "application/json",
"total": total
});
if (request.method === "HEAD") {
response.end();
return;
}
response.write("[\n");
i = 0;
cur.each((err, item) => {
if (err) {
throwError(err);
return false;
}
if (item) {
if (i++)
response.write(",\n");
response.write(JSON.stringify(item));
}
if (!item || i >= limit) {
response.end("\n]");
return false;
}
});
});
} else {
response.writeHead(404);
response.end("404 Not Found");
}
});
}
exports.listener = listener;