diff --git a/.gitignore b/.gitignore
index 30210d1..1536944 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,6 @@
lib/*.map
lib/cwmp.js
lib/db.js
-lib/nbi.js
lib/session.js
node_modules
debug
diff --git a/lib/nbi.coffee b/lib/nbi.coffee
deleted file mode 100644
index 840c03a..0000000
--- a/lib/nbi.coffee
+++ /dev/null
@@ -1,516 +0,0 @@
-###
-# Copyright 2013-2017 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 .
-#
-# This file incorporates work covered by the following copyright and
-# permission notice:
-#
-# Copyright 2013 Fanoos Telecom
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-###
-
-url = require 'url'
-mongodb = require 'mongodb'
-querystring = require 'querystring'
-vm = require 'vm'
-
-config = require './config'
-common = require './common'
-db = require './db'
-query = require './query'
-apiFunctions = require './api-functions'
-cache = require './cache'
-
-VERSION = require('../package.json').version
-
-# regular expression objects
-DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tasks\/?$/
-TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-\_\%]+)(\/[a-zA-Z_]*)?$/
-TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tags\/([a-zA-Z0-9\-\_\%]+)\/?$/
-PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-\_\%]+)\/?$/
-OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-\_\%]+)\/?$/
-FILES_REGEX = /^\/files\/([a-zA-Z0-9\%\!\*\'\(\)\;\:\@\&\=\+\$\,\?\#\[\]\-\_\.\~]+)\/?$/
-PING_REGEX = /^\/ping\/([a-zA-Z0-9\-\_\.\:]+)\/?$/
-QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/
-DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/
-PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-\_\%]+)\/?$/
-VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-\_\%]+)\/?$/
-FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-\_\%\:]+)\/?$/
-
-
-
-throwError = (err, httpResponse) ->
- if httpResponse
- httpResponse.writeHead(500, {'Connection' : 'close'})
- httpResponse.end("#{err.name}: #{err.message}")
-
- throw err
-
-
-listener = (request, response) ->
- chunks = []
- bytes = 0
-
- response.setHeader('GenieACS-Version', VERSION)
-
- request.addListener 'data', (chunk) ->
- chunks.push(chunk)
- bytes += chunk.length
-
- request.getBody = () ->
- # Write all chunks into a Buffer
- body = new Buffer(bytes)
- offset = 0
- chunks.forEach((chunk) ->
- chunk.copy(body, offset, 0, chunk.length)
- offset += chunk.length
- )
- return body
-
- request.addListener 'end', () ->
- body = request.getBody()
- urlParts = url.parse(request.url, true)
- if PRESETS_REGEX.test(urlParts.pathname)
- presetName = querystring.unescape(PRESETS_REGEX.exec(urlParts.pathname)[1])
- if request.method == 'PUT'
- preset = JSON.parse(body)
- preset._id = presetName
-
- db.presetsCollection.save(preset, (err) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- )
- else if request.method == 'DELETE'
- db.presetsCollection.remove({'_id' : presetName}, (err) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- 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)
- objectName = querystring.unescape(OBJECTS_REGEX.exec(urlParts.pathname)[1])
- if request.method == 'PUT'
- object = JSON.parse(body)
- object._id = objectName
-
- db.objectsCollection.save(object, (err) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- )
- else if request.method == 'DELETE'
- db.objectsCollection.remove({'_id' : objectName}, (err) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- 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)
- provisionName = querystring.unescape(PROVISIONS_REGEX.exec(urlParts.pathname)[1])
- if request.method == 'PUT'
- 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) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- )
- else if request.method == 'DELETE'
- db.provisionsCollection.remove({'_id' : provisionName}, (err) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- 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)
- virtualParameterName = querystring.unescape(VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1])
- if request.method == 'PUT'
- 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) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- )
- else if request.method == 'DELETE'
- db.virtualParametersCollection.remove({'_id' : virtualParameterName}, (err) ->
- return throwError(err, response) if err
- cache.del('presets_hash', (err) ->
- return throwError(err, response) if err
- 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)
- r = TAGS_REGEX.exec(urlParts.pathname)
- deviceId = querystring.unescape(r[1])
- tag = querystring.unescape(r[2])
- if request.method == 'POST'
- db.devicesCollection.update({'_id' : deviceId}, {'$addToSet' : {'_tags' : tag}}, {safe: true}, (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- else if request.method == 'DELETE'
- db.devicesCollection.update({'_id' : deviceId}, {'$pull' : {'_tags' : tag}}, {safe: true}, (err) ->
- return throwError(err, response) if err
- 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'
- faultId = querystring.unescape(FAULTS_REGEX.exec(urlParts.pathname)[1])
- deviceId = faultId.split(':', 1)[0]
- channel = faultId.slice(deviceId.length + 1)
- db.faultsCollection.remove({_id : faultId}, (err) ->
- return throwError(err, response) if err
- if channel.startsWith('task_')
- return db.tasksCollection.remove({_id : new mongodb.ObjectID(channel.slice(5))}, (err) ->
- return throwError(err, response) if err
- cache.del("#{deviceId}_tasks_faults_operations", (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- )
-
- cache.del("#{deviceId}_tasks_faults_operations", (err) ->
- return throwError(err, response) if err
- 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'
- deviceId = querystring.unescape(DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1])
- if body.length
- task = JSON.parse(body)
- task.device = deviceId
- apiFunctions.insertTasks(task, (err) ->
- return throwError(err, response) if err
- cache.del("#{deviceId}_tasks_faults_operations", (err) ->
- return throwError(err, response) if err
-
- if urlParts.query.connection_request?
- apiFunctions.connectionRequest(deviceId, (err) ->
- if err
- response.writeHead(202, err.message, {'Content-Type' : 'application/json'})
- response.end(JSON.stringify(task))
- else
- apiFunctions.watchTask(deviceId, task._id, config.get('DEVICE_ONLINE_THRESHOLD', deviceId), (err, status) ->
- return throwError(err, response) if err
-
- if status is 'timeout'
- response.writeHead(202, 'Task queued but not processed', {'Content-Type' : 'application/json'})
- response.end(JSON.stringify(task))
- else if status is 'fault'
- db.tasksCollection.findOne({_id : task._id}, (err, task) ->
- return throwError(err, response) if err
-
- 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?
- # 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)
- r = TASKS_REGEX.exec(urlParts.pathname)
- taskId = new mongodb.ObjectID(querystring.unescape(r[1]))
- action = r[2]
- if not action? or action is '/'
- if request.method == 'DELETE'
- db.tasksCollection.findOne({'_id' : taskId}, {'device' : 1}, (err, task) ->
- return throwError(err, response) if err
- if not task?
- response.writeHead(404)
- response.end("Task not found")
- return
-
- deviceId = task.device
- db.tasksCollection.remove({'_id' : taskId}, (err) ->
- return throwError(err, response) if err
- db.faultsCollection.remove({_id : "#{deviceId}:task_#{String(taskId)}"}, (err) ->
- return throwError(err, response) if err
- cache.del("#{deviceId}_tasks_faults_operations", (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- )
- )
- )
- else
- response.writeHead 405, {'Allow': 'PUT DELETE'}
- response.end('405 Method Not Allowed')
- else if action is '/retry'
- if request.method == 'POST'
- db.tasksCollection.findOne({'_id' : taskId}, {'device' : 1}, (err, task) ->
- return throwError(err, response) if err
- deviceId = task.device
- db.faultsCollection.remove({_id : "#{deviceId}:task_#{String(taskId)}"}, (err) ->
- return throwError(err, response) if err
- cache.del("#{deviceId}_tasks_faults_operations", (err) ->
- return throwError(err, response) if err
- 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)
- filename = querystring.unescape(FILES_REGEX.exec(urlParts.pathname)[1])
- if request.method == 'PUT'
- metadata = {
- fileType : request.headers.filetype,
- oui : request.headers.oui,
- productClass : request.headers.productclass,
- version : request.headers.version,
- }
-
- gs = new mongodb.GridStore(db.mongoDb, filename, filename, 'w', {metadata : metadata})
- gs.open((err, gs) ->
- gs.write(body, (err, res) ->
- return throwError(err, response) if err
- gs.close((err) ->
- return throwError(err, response) if err
- response.writeHead(201)
- response.end()
- )
- )
- )
- else if request.method == 'DELETE'
- mongodb.GridStore.unlink(db.mongoDb, filename, (err) ->
- 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)
- host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1])
- require('child_process').exec("ping -w 1 -i 0.2 -c 3 #{host}", (err, stdout, stderr) ->
- 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 isnt 'DELETE'
- response.writeHead(405, {'Allow' : 'DELETE'})
- response.end('405 Method Not Allowed')
- return
-
- deviceId = querystring.unescape(DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1])
- apiFunctions.deleteDevice(deviceId, (err) ->
- return throwError(err, response) if err
- response.writeHead(200)
- response.end()
- )
- else if QUERY_REGEX.test(urlParts.pathname)
- collectionName = QUERY_REGEX.exec(urlParts.pathname)[1]
-
- # Convert to camel case
- i = collectionName.indexOf('_')
- while i >= 0
- ++ i
- up = if i < collectionName.length then collectionName[i].toUpperCase() else ''
- collectionName = collectionName.slice(0, i - 1) + up + collectionName.slice(i + 1)
- i = collectionName.indexOf('_', i)
-
- if request.method not in ['GET', 'HEAD']
- response.writeHead 405, {'Allow' : 'GET, HEAD'}
- response.end('405 Method Not Allowed')
- return
- collection = db["#{collectionName}Collection"]
- if not collection?
- response.writeHead 404
- response.end('404 Not Found')
- return
-
- if urlParts.query.query?
- try
- q = JSON.parse(urlParts.query.query)
- catch err
- response.writeHead(400)
- response.end("#{err.name}: #{err.message}")
- return
- else
- q = {}
-
- switch collectionName
- when 'devices'
- q = query.expand(q)
- when 'tasks'
- q = query.sanitizeQueryTypes(q, {
- _id: ((v) -> return new mongodb.ObjectID(v))
- timestamp: ((v) -> return new Date(v))
- retries: Number
- })
- when 'faults'
- q = query.sanitizeQueryTypes(q, {
- timestamp: ((v) -> return new Date(v))
- retries: Number
- })
-
- if urlParts.query.projection?
- projection = {}
- for p in urlParts.query.projection.split(',')
- p = p.trim()
- projection[p] = 1
-
- cur = collection.find(q, projection, {batchSize : 50})
- if urlParts.query.sort?
- s = JSON.parse(urlParts.query.sort)
- sort = {}
- for k,v of s
- if k[k.lastIndexOf('.') + 1] != '_' and collectionName is 'devices'
- sort["#{k}._value"] = v
- else
- sort[k] = v
- cur.sort(sort)
-
- cur.skip(parseInt(urlParts.query.skip)) if urlParts.query.skip?
- cur.limit(limit = parseInt(urlParts.query.limit)) if urlParts.query.limit?
- cur.count(false, (err, total) ->
- response.writeHead(200, {'Content-Type' : 'application/json', 'total' : total})
- if request.method is 'HEAD'
- response.end()
- return
- response.write("[\n")
- i = 0
- cur.each((err, item) ->
- if err
- throwError(err)
- return false
-
- if item?
- response.write(",\n") if i++
- response.write(JSON.stringify(item))
-
- if not item? or (limit? and i >= limit)
- response.end("\n]")
- )
- )
-
- else
- response.writeHead 404
- response.end('404 Not Found')
-
-
-exports.listener = listener
diff --git a/lib/nbi.js b/lib/nbi.js
new file mode 100644
index 0000000..52af579
--- /dev/null
+++ b/lib/nbi.js
@@ -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 .
+ */
+"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;