Convert nbi.coffee to JS
This commit is contained in:
1 parent
91fb6c1c5a
commit
c836c73ffe
3 files changed
+648
-517
No files matched your search
-516
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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
|
||||
+648
@@ -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;
|
||||
Reference in new issue
Block a user