diff --git a/.gitignore b/.gitignore
index 3086bbc..30210d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
*~
lib/*.map
-lib/api-functions.js
lib/cwmp.js
lib/db.js
lib/nbi.js
diff --git a/lib/api-functions.coffee b/lib/api-functions.coffee
deleted file mode 100644
index e810136..0000000
--- a/lib/api-functions.coffee
+++ /dev/null
@@ -1,265 +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 .
-###
-
-crypto = require 'crypto'
-dgram = require 'dgram'
-
-config = require './config'
-db = require './db'
-http = require 'http'
-common = require './common'
-util = require 'util'
-URL = require 'url'
-auth = require './auth'
-cache = require './cache'
-
-
-udpConReq = (address, un, key, callback) ->
- return false if not address
- [host, port] = address.split(':', 2)
- if port is undefined
- port = 80
- else
- port = parseInt(port)
- return false if isNaN(port)
-
- ts = Math.trunc(Date.now() / 1000)
- id = Math.trunc(Math.random() * 4294967295)
- cn = crypto.randomBytes(8).toString('hex')
- sig = crypto.createHmac('sha1', key).update("#{ts}#{id}#{un}#{cn}").digest('hex')
- uri = "http://#{address}?ts=#{ts}&id=#{id}&un=#{un}&cn=#{cn}&sig=#{sig}"
-
- message = Buffer.from("GET #{uri} HTTP/1.1\r\nHost: #{address}\r\n\r\n")
-
- client = dgram.createSocket({type: 'udp4', reuseAddr: true})
- UDP_CONNECTION_REQUEST_PORT = config.get('UDP_CONNECTION_REQUEST_PORT')
- if UDP_CONNECTION_REQUEST_PORT
- # When a device is NAT'ed, the UDP Connection Request must originate from the same address and port used by the STUN server, in order to traverse the firewall.
- # This does require that the Genieacs NBI and STUN server are allowed to bind to the same address and port.
- # The STUN server needs to open its UDP port with the SO_REUSEADDR option, allowing the NBI to also bind to the same port.
- client.bind({port: UDP_CONNECTION_REQUEST_PORT, exclusive: true})
-
- count = 3
- client.send(message, 0, message.length, port, host, f = (err) ->
- if err or -- count <= 0
- client.close()
- return callback(err)
- client.send(message, 0, message.length, port, host, f)
- )
- return true
-
-
-httpConReq = (url, username, password, allowBasicAuth, timeout, callback) ->
- options = URL.parse(url)
-
- if options.protocol != 'http:'
- return callback(new Error('Invalid connection request URL or protocol'))
-
- # Ensure socket is reused in case of digest authentication
- options.agent = new http.Agent({maxSockets : 1})
-
- statusToError = (statusCode) ->
- switch statusCode
- when 200, 204
- null
- when 401
- new Error('Incorrect connection request credentials')
- when 0
- new Error('Device is offline')
- else
- new Error("Unexpected response code from device: #{statusCode}")
-
- request = http.get(options, (res) ->
- if res.statusCode == 401 and res.headers['www-authenticate']?
- authHeader = auth.parseAuthHeader(res.headers['www-authenticate'])
- if authHeader.method is 'Basic'
- if not allowBasicAuth
- request.abort()
- callback(new Error('Basic HTTP authentication not allowed'))
- return
- options.headers = {'Authorization' : auth.basic(username or '', password or '')}
- else if authHeader.method is 'Digest'
- options.headers = {
- 'Authorization' : auth.digest(username or '', password or '', options.path, 'GET', null, authHeader)
- }
-
- request = http.get(options, (res) ->
- if res.statusCode == 0
- # Workaround for some devices unexpectedly closing the connection
- request = http.get(options, (res) ->
- callback(statusToError(res.statusCode))
- res.resume()
- ).on('error', (err) ->
- request.abort()
- callback(statusToError(0))
- ).on('socket', (socket) ->
- socket.setTimeout(timeout)
- socket.on('timeout', () -> request.abort())
- )
- else
- callback(statusToError(res.statusCode))
- res.resume()
- ).on('error', (err) ->
- request.abort()
- callback(statusToError(0))
- ).on('socket', (socket) ->
- socket.setTimeout(timeout)
- socket.on('timeout', () -> request.abort())
- )
- else
- callback(statusToError(res.statusCode))
-
- # No listener for data so emit resume
- res.resume()
- ).on('error', (err) ->
- request.abort()
- callback(statusToError(0))
- ).on('socket', (socket) ->
- socket.setTimeout(timeout)
- socket.on('timeout', () -> request.abort())
- )
-
-
-connectionRequest = (deviceId, callback) ->
- CONNECTION_REQUEST_TIMEOUT = config.get('CONNECTION_REQUEST_TIMEOUT', deviceId)
- CONNECTION_REQUEST_ALLOW_BASIC_AUTH = config.get('CONNECTION_REQUEST_ALLOW_BASIC_AUTH', deviceId)
- proj = {
- 'Device.ManagementServer.ConnectionRequestURL._value' : 1,
- 'Device.ManagementServer.UDPConnectionRequestAddress._value' : 1,
- 'Device.ManagementServer.ConnectionRequestUsername._value' : 1,
- 'Device.ManagementServer.ConnectionRequestPassword._value' : 1,
- 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1,
- 'InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value' : 1,
- 'InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value' : 1,
- 'InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value' : 1
- }
-
- db.devicesCollection.findOne({_id : deviceId}, proj, (err, device)->
- return callback(err) if err
-
- if device.Device? # TR-181 data model
- connectionRequestUrl = device.Device.ManagementServer.ConnectionRequestURL._value
- udpConnectionRequestAddress = device.Device.ManagementServer?.UDPConnectionRequestAddress?._value
- username = device.Device.ManagementServer.ConnectionRequestUsername?._value
- password = device.Device.ManagementServer.ConnectionRequestPassword?._value
- else # TR-098 data model
- connectionRequestUrl = device.InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value
- udpConnectionRequestAddress = device.InternetGatewayDevice.ManagementServer?.UDPConnectionRequestAddress?._value
- username = device.InternetGatewayDevice.ManagementServer.ConnectionRequestUsername?._value
- password = device.InternetGatewayDevice.ManagementServer.ConnectionRequestPassword?._value
-
- conReq = () ->
- udpSent = udpConReq(udpConnectionRequestAddress, username, password, (err) -> throw err if err)
-
- httpConReq(connectionRequestUrl, username, password, CONNECTION_REQUEST_ALLOW_BASIC_AUTH, CONNECTION_REQUEST_TIMEOUT, (err) ->
- if udpSent
- return callback()
- callback(err)
- )
-
- if config.auth?.connectionRequest?
- # Callback is optional for backward compatibility
- if config.auth.connectionRequest.length > 4
- return config.auth.connectionRequest(deviceId, connectionRequestUrl, username, password, (u, p) ->
- username = u
- password = p
- conReq()
- )
- [username, password] = config.auth.connectionRequest(deviceId, connectionRequestUrl, username, password)
- conReq()
- )
-
-
-watchTask = (deviceId, taskId, timeout, callback) ->
- setTimeout(() ->
- db.tasksCollection.findOne({_id : taskId}, {'_id' : 1}, (err, task) ->
- return callback(err) if err
-
- if not task
- return callback(null, 'completed')
-
- db.faultsCollection.findOne({_id : "#{deviceId}:task_#{taskId}"}, {'_id' : 1}, (err, fault) ->
- return callback(err) if err
-
- if fault
- return callback(null, 'fault')
-
- if (timeout -= 500) <= 0
- return callback(null, 'timeout')
-
- watchTask(deviceId, taskId, timeout, callback)
- )
- )
- , 500)
-
-
-sanitizeTask = (task, callback) ->
- task.timestamp = new Date(task.timestamp ? Date.now())
- if task.expiry?
- if common.typeOf(task.expiry) is common.DATE_TYPE or isNaN(task.expiry)
- task.expiry = new Date(task.expiry)
- else
- task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000)
-
- callback(task)
-
-
-insertTasks = (tasks, callback) ->
- if tasks? and common.typeOf(tasks) isnt common.ARRAY_TYPE
- tasks = [tasks]
- else if not tasks? or tasks.length == 0
- return callback(null, tasks or [])
-
- counter = tasks.length
-
- for task in tasks
- sanitizeTask(task, (t) ->
- if t.uniqueKey?
- db.tasksCollection.remove({device : t.device, uniqueKey : t.uniqueKey}, (err) ->
- )
-
- --counter
- if counter == 0
- db.tasksCollection.insert(tasks, (err, _tasks) ->
- #util.log("#{_task.device}: Added task #{_task.name}(#{_task._id})") for _task in _tasks
- callback(err, _tasks)
- )
- )
-
-
-deleteDevice = (deviceId, callback) ->
- db.tasksCollection.remove({'device' : deviceId}, (err) ->
- return callback(err) if err
- db.devicesCollection.remove({'_id' : deviceId}, (err) ->
- return callback(err) if err
- db.faultsCollection.remove({'_id' : {'$regex' : "^#{common.escapeRegExp(deviceId)}\\:"}}, (err) ->
- return callback(err) if err
- db.operationsCollection.remove({'_id' : {'$regex' : "^#{common.escapeRegExp(deviceId)}\\:"}}, (err) ->
- return callback(err) if err
- cache.del("#{deviceId}_tasks_faults_operations", callback)
- )
- )
- )
- )
-
-
-exports.connectionRequest = connectionRequest
-exports.watchTask = watchTask
-exports.insertTasks = insertTasks
-exports.deleteDevice = deleteDevice
diff --git a/lib/api-functions.js b/lib/api-functions.js
new file mode 100644
index 0000000..74af120
--- /dev/null
+++ b/lib/api-functions.js
@@ -0,0 +1,301 @@
+/**
+ * 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 crypto = require("crypto");
+const dgram = require("dgram");
+const URL = require("url");
+const http = require("http");
+const config = require("./config");
+const db = require("./db");
+const common = require("./common");
+const auth = require("./auth");
+const cache = require("./cache");
+
+function udpConReq(address, un, key, callback) {
+ if (!address)
+ return false;
+
+ let [host, port] = address.split(":", 2);
+
+ if (!port) {
+ port = 80;
+ } else {
+ port = parseInt(port);
+ if (isNaN(port))
+ return false;
+ }
+
+ const ts = Math.trunc(Date.now() / 1000);
+ const id = Math.trunc(Math.random() * 4294967295);
+ const cn = crypto.randomBytes(8).toString("hex");
+ const sig = crypto.createHmac("sha1", key).update(`${ts}${id}${un}${cn}`).digest("hex");
+ const uri = `http://${address}?ts=${ts}&id=${id}&un=${un}&cn=${cn}&sig=${sig}`;
+ const message = Buffer.from(`GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`);
+
+ const client = dgram.createSocket({type: "udp4", reuseAddr: true});
+ const UDP_CONNECTION_REQUEST_PORT = config.get("UDP_CONNECTION_REQUEST_PORT");
+
+ if (UDP_CONNECTION_REQUEST_PORT)
+ // When a device is NAT'ed, the UDP Connection Request must originate from
+ // the same address and port used by the STUN server, in order to traverse
+ // the firewall. This does require that the Genieacs NBI and STUN server
+ // are allowed to bind to the same address and port. The STUN server needs
+ // to open its UDP port with the SO_REUSEADDR option, allowing the NBI to
+ // also bind to the same port.
+ client.bind({port: UDP_CONNECTION_REQUEST_PORT, exclusive: true});
+
+ let count = 3;
+ const func = (err) => {
+ if (err || --count <= 0) {
+ client.close();
+ return callback(err);
+ }
+ client.send(message, 0, message.length, port, host, func);
+ };
+ client.send(message, 0, message.length, port, host, func);
+ return true;
+}
+
+function httpConReq(url, username, password, allowBasicAuth, timeout, callback) {
+ const options = URL.parse(url);
+ if (options.protocol !== "http:")
+ return callback(new Error("Invalid connection request URL or protocol"));
+ options.agent = new http.Agent({maxSockets: 1});
+
+ function statusToError(statusCode) {
+ switch (statusCode) {
+ case 200:
+ case 204:
+ return null;
+ case 401:
+ return new Error("Incorrect connection request credentials");
+ case 0:
+ return new Error("Device is offline");
+ default:
+ return new Error(`Unexpected response code from device: ${statusCode}`);
+ }
+ }
+
+ const request = http.get(options, (res) => {
+ if (res.statusCode === 401 && res.headers["www-authenticate"]) {
+ const authHeader = auth.parseAuthHeader(res.headers["www-authenticate"]);
+ if (authHeader.method === "Basic") {
+ if (!allowBasicAuth) {
+ request.abort();
+ return callback(new Error("Basic HTTP authentication not allowed"));
+ }
+ options.headers = {
+ "Authorization": auth.basic(username || "", password || "")
+ };
+ } else if (authHeader.method === "Digest") {
+ options.headers = {
+ "Authorization": auth.digest(username || "", password || "", options.path, "GET", null, authHeader)
+ };
+ }
+
+ const request = http.get(options, (res) => {
+ if (res.statusCode === 0) {
+ // Workaround for some devices unexpectedly closing the connection
+ request = http.get(options, (res) => {
+ callback(statusToError(res.statusCode));
+ res.resume();
+ }).on("error", () => {
+ request.abort();
+ callback(statusToError(0));
+ }).on("socket", (socket) => {
+ socket.setTimeout(timeout);
+ socket.on("timeout", () => {
+ request.abort();
+ });
+ });
+ } else {
+ callback(statusToError(res.statusCode));
+ }
+ res.resume();
+ }).on("error", () => {
+ request.abort();
+ callback(statusToError(0));
+ }).on("socket", (socket) => {
+ socket.setTimeout(timeout);
+ socket.on("timeout", () => {
+ request.abort();
+ });
+ });
+ } else {
+ callback(statusToError(res.statusCode));
+ }
+ // No listener for data so emit resume
+ res.resume();
+ }).on("error", (err) => {
+ request.abort();
+ callback(statusToError(0));
+ }).on("socket", (socket) => {
+ socket.setTimeout(timeout);
+ socket.on("timeout", () => {
+ request.abort();
+ });
+ });
+}
+
+function connectionRequest(deviceId, callback) {
+ const CONNECTION_REQUEST_TIMEOUT = config.get("CONNECTION_REQUEST_TIMEOUT", deviceId);
+ const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = config.get("CONNECTION_REQUEST_ALLOW_BASIC_AUTH", deviceId);
+ const proj = {
+ "Device.ManagementServer.ConnectionRequestURL._value": 1,
+ "Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
+ "Device.ManagementServer.ConnectionRequestUsername._value": 1,
+ "Device.ManagementServer.ConnectionRequestPassword._value": 1,
+ "InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
+ "InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
+ "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
+ "InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
+ };
+
+ db.devicesCollection.findOne({_id: deviceId}, proj, (err, device) => {
+ if (err)
+ return callback(err);
+
+ let managementServer, connectionRequestUrl, udpConnectionRequestAddress, username, password;
+ if (device.Device) // TR-181 data model
+ managementServer = device.Device.ManagementServer;
+ else // TR-098 data model
+ managementServer = device.InternetGatewayDevice.ManagementServer;
+
+ connectionRequestUrl = managementServer.ConnectionRequestURL._value;
+ if (managementServer.UDPConnectionRequestAddress)
+ udpConnectionRequestAddress = managementServer.UDPConnectionRequestAddress._value;
+ if (managementServer.ConnectionRequestUsername)
+ username = managementServer.ConnectionRequestUsername._value;
+ if (managementServer.ConnectionRequestPassword)
+ password = managementServer.ConnectionRequestPassword._value;
+
+ const conReq = () => {
+ const udpSent = udpConReq(udpConnectionRequestAddress, username, password, (err) => {
+ if (err)
+ throw err;
+ });
+
+ httpConReq(connectionRequestUrl, username, password, CONNECTION_REQUEST_ALLOW_BASIC_AUTH, CONNECTION_REQUEST_TIMEOUT, (err) => {
+ if (udpSent)
+ return callback();
+ callback(err);
+ });
+ };
+
+ if (config.auth && config.auth.connectionRequest) {
+ // Callback is optional for backward compatibility
+ if (config.auth.connectionRequest.length > 4)
+ return config.auth.connectionRequest(deviceId, connectionRequestUrl, username, password, (u, p) => {
+ username = u;
+ password = p;
+ conReq();
+ });
+ [username, password] = config.auth.connectionRequest(deviceId, connectionRequestUrl, username, password);
+ }
+ conReq();
+ });
+}
+
+function watchTask(deviceId, taskId, timeout, callback) {
+ setTimeout(() => {
+ db.tasksCollection.findOne({_id: taskId}, {_id: 1}, (err, task) => {
+ if (err)
+ return callback(err);
+
+ if (!task)
+ return callback(null, "completed");
+
+ const q = {_id: `${deviceId}:task_${taskId}`};
+ db.faultsCollection.findOne(q, {_id: 1}, (err, fault) => {
+ if (err)
+ return callback(err);
+
+ if (fault)
+ return callback(null, "fault");
+
+ if ((timeout -= 500) <= 0)
+ return callback(null, "timeout");
+
+ watchTask(deviceId, taskId, timeout, callback);
+ });
+ });
+ }, 500);
+}
+
+function sanitizeTask(task, callback) {
+ task.timestamp = new Date(task.timestamp || Date.now());
+ if (task.expiry) {
+ if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry))
+ task.expiry = new Date(task.expiry);
+ else
+ task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000);
+ }
+ callback(task);
+}
+
+function insertTasks(tasks, callback) {
+ if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE)
+ tasks = [tasks];
+ else if (!tasks || tasks.length === 0)
+ return callback(null, tasks || []);
+
+ let counter = tasks.length;
+ for (let task of tasks)
+ sanitizeTask(task, (t) => {
+ if (t.uniqueKey)
+ db.tasksCollection.remove({device: t.device, uniqueKey: t.uniqueKey}, () => {});
+
+ if (--counter === 0)
+ db.tasksCollection.insert(tasks, callback);
+ });
+}
+
+function deleteDevice(deviceId, callback) {
+ db.tasksCollection.remove({"device": deviceId}, (err) => {
+ if (err)
+ return callback(err);
+ db.devicesCollection.remove({"_id": deviceId}, (err) => {
+ if (err)
+ return callback(err);
+ db.faultsCollection.remove({
+ "_id": {
+ "$regex": `^${common.escapeRegExp(deviceId)}\\:`
+ }
+ }, (err) => {
+ if (err)
+ return callback(err);
+ db.operationsCollection.remove({
+ "_id": {
+ "$regex": `^${common.escapeRegExp(deviceId)}\\:`
+ }
+ }, (err) => {
+ if (err)
+ return callback(err);
+ cache.del(`${deviceId}_tasks_faults_operations`, callback);
+ });
+ });
+ });
+ });
+}
+
+exports.connectionRequest = connectionRequest;
+exports.watchTask = watchTask;
+exports.insertTasks = insertTasks;
+exports.deleteDevice = deleteDevice;