From 91fb6c1c5afe5ea6f4b4a137c4dff383b52df509 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Tue, 21 Aug 2018 11:03:19 -0700 Subject: [PATCH] Convert api-functions.coffee to JS --- .gitignore | 1 - lib/api-functions.coffee | 265 ---------------------------------- lib/api-functions.js | 301 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 301 insertions(+), 266 deletions(-) delete mode 100644 lib/api-functions.coffee create mode 100644 lib/api-functions.js 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;