diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e47a4ca --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "script", + "ecmaVersion": 8 + }, + "rules": { + "no-shadow": ["error", { "allow": ["err"] }], + "handle-callback-err": "error", + "prefer-arrow-callback": "error", + "curly": ["error", "multi-or-nest", "consistent"] + } +} diff --git a/.npmignore b/.npmignore index fa3cb51..abca22c 100644 --- a/.npmignore +++ b/.npmignore @@ -16,3 +16,5 @@ config/ext debug/* !debug/.gitkeep + +.eslintrc.json diff --git a/lib/api-functions.js b/lib/api-functions.js index 74af120..d62702e 100644 --- a/lib/api-functions.js +++ b/lib/api-functions.js @@ -29,8 +29,7 @@ const auth = require("./auth"); const cache = require("./cache"); function udpConReq(address, un, key, callback) { - if (!address) - return false; + if (!address) return false; let [host, port] = address.split(":", 2); @@ -38,18 +37,22 @@ function udpConReq(address, un, key, callback) { port = 80; } else { port = parseInt(port); - if (isNaN(port)) - return false; + 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 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 message = Buffer.from( + `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n` + ); - const client = dgram.createSocket({type: "udp4", reuseAddr: true}); + const client = dgram.createSocket({ type: "udp4", reuseAddr: true }); const UDP_CONNECTION_REQUEST_PORT = config.get("UDP_CONNECTION_REQUEST_PORT"); if (UDP_CONNECTION_REQUEST_PORT) @@ -59,10 +62,10 @@ function udpConReq(address, un, key, callback) { // 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}); + client.bind({ port: UDP_CONNECTION_REQUEST_PORT, exclusive: true }); let count = 3; - const func = (err) => { + const func = err => { if (err || --count <= 0) { client.close(); return callback(err); @@ -73,11 +76,18 @@ function udpConReq(address, un, key, callback) { return true; } -function httpConReq(url, username, password, allowBasicAuth, timeout, callback) { +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}); + options.agent = new http.Agent({ maxSockets: 1 }); function statusToError(statusCode) { switch (statusCode) { @@ -93,70 +103,96 @@ function httpConReq(url, username, password, allowBasicAuth, timeout, callback) } } - 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", () => { + const request = http + .get(options, response => { + if (response.statusCode === 401 && response.headers["www-authenticate"]) { + const authHeader = auth.parseAuthHeader( + response.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 req = http + .get(options, res => { + if (res.statusCode === 0) { + // Workaround for some devices unexpectedly closing the connection + let req2; + req2 = http + .get(options, res2 => { + callback(statusToError(res2.statusCode)); + res2.resume(); + }) + .on("error", () => { + req2.abort(); + callback(statusToError(0)); + }) + .on("socket", socket => { + socket.setTimeout(timeout); + socket.on("timeout", () => { + req2.abort(); + }); + }); + req2(); + } else { + callback(statusToError(res.statusCode)); + } + res.resume(); + }) + .on("error", () => { + req.abort(); callback(statusToError(0)); - }).on("socket", (socket) => { + }) + .on("socket", socket => { socket.setTimeout(timeout); socket.on("timeout", () => { - request.abort(); + req.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", () => { + } else { + callback(statusToError(response.statusCode)); + } + // No listener for data so emit resume + response.resume(); + }) + .on("error", () => { 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 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, @@ -168,46 +204,74 @@ function connectionRequest(deviceId, callback) { "InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1 }; - db.devicesCollection.findOne({_id: deviceId}, proj, (err, device) => { - if (err) - return callback(err); + 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 + 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; + // TR-098 data model + else managementServer = device.InternetGatewayDevice.ManagementServer; connectionRequestUrl = managementServer.ConnectionRequestURL._value; - if (managementServer.UDPConnectionRequestAddress) - udpConnectionRequestAddress = managementServer.UDPConnectionRequestAddress._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; - }); + 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); - }); + 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); + 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(); }); @@ -215,23 +279,18 @@ function connectionRequest(deviceId, callback) { function watchTask(deviceId, taskId, timeout, callback) { setTimeout(() => { - db.tasksCollection.findOne({_id: taskId}, {_id: 1}, (err, task) => { - if (err) - return callback(err); + db.tasksCollection.findOne({ _id: taskId }, { _id: 1 }, (err, task) => { + if (err) return callback(err); - if (!task) - return callback(null, "completed"); + 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); + 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 (fault) return callback(null, "fault"); - if ((timeout -= 500) <= 0) - return callback(null, "timeout"); + if ((timeout -= 500) <= 0) return callback(null, "timeout"); watchTask(deviceId, taskId, timeout, callback); }); @@ -244,53 +303,57 @@ function sanitizeTask(task, callback) { 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); + 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 || []); + 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}, () => {}); + 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); + 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)}\\:` + 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); - cache.del(`${deviceId}_tasks_faults_operations`, callback); - }); - }); + }, + 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); + } + ); + } + ); }); }); } diff --git a/lib/auth.js b/lib/auth.js index c390269..744f9da 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -23,7 +23,7 @@ const crypto = require("crypto"); function parseAuthHeader(authHeader) { authHeader = authHeader.trim(); const method = authHeader.split(" ", 1)[0]; - const res = {method: method}; + const res = { method: method }; const parts = authHeader.slice(method.length + 1).split(","); let part; @@ -38,8 +38,7 @@ function parseAuthHeader(authHeader) { } else { while (!/[^\\]"\s*$/.test(value)) { const p = parts.shift(); - if (p == null) - throw new Error("Unable to parse auth header"); + if (p == null) throw new Error("Unable to parse auth header"); value += "," + p; } @@ -55,7 +54,7 @@ function parseAuthHeader(authHeader) { } function basic(username, password) { - return "Basic " + (new Buffer(`${username}:${password}`).toString("base64")); + return "Basic " + new Buffer(`${username}:${password}`).toString("base64"); } function digest(username, password, uri, httpMethod, body, authHeader) { @@ -64,40 +63,53 @@ function digest(username, password, uri, httpMethod, body, authHeader) { let qop; if (authHeader.qop) { - if (authHeader.qop.indexOf(",") !== -1) - qop = "auth"; // Either auth or auth-int, prefer auth - else - qop = authHeader.qop; + if (authHeader.qop.indexOf(",") !== -1) qop = "auth"; + // Either auth or auth-int, prefer auth + else qop = authHeader.qop; } let ha1 = crypto.createHash("md5"); - ha1.update(username).update(":").update(authHeader.realm).update(":").update(password); + ha1 + .update(username) + .update(":") + .update(authHeader.realm) + .update(":") + .update(password); // TODO support "MD5-sess" algorithm directive ha1 = ha1.digest("hex"); let ha2 = crypto.createHash("md5"); - ha2.update(httpMethod).update(":").update(uri); - if (qop === "auth-int") - ha2.update(":").update(body); + ha2 + .update(httpMethod) + .update(":") + .update(uri); + if (qop === "auth-int") ha2.update(":").update(body); ha2 = ha2.digest("hex"); let hash = crypto.createHash("md5"); - hash.update(ha1).update(":").update(authHeader.nonce); - if (qop) - hash.update(":").update(nc).update(":").update(cnonce).update(":").update(qop); + hash + .update(ha1) + .update(":") + .update(authHeader.nonce); + if (qop) { + hash + .update(":") + .update(nc) + .update(":") + .update(cnonce) + .update(":") + .update(qop); + } hash.update(":").update(ha2); let authString = `Digest username="${username}"`; authString += `,realm="${authHeader.realm}"`; authString += `,nonce="${authHeader.nonce}"`; - authString += `,uri="${url}"`; - if (authHeader.algorithm) - authString += `,algorithm=${authHeader.algorithm}`; - if (qop) - authString += `,qop=${qop},nc=${nc},cnonce="${cnonce}"`; + authString += `,uri="${uri}"`; + if (authHeader.algorithm) authString += `,algorithm=${authHeader.algorithm}`; + if (qop) authString += `,qop=${qop},nc=${nc},cnonce="${cnonce}"`; authString += `,response="${hash.digest("hex")}"`; - if (authHeader.opaque) - authString += `,opaque="${authHeader.opaque}"`; + if (authHeader.opaque) authString += `,opaque="${authHeader.opaque}"`; return authString; } diff --git a/lib/cache.js b/lib/cache.js index b2f4d76..d9a443c 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -27,7 +27,6 @@ let mongoDb; let mongoCollection; let mongoTimeOffset = 0; - function connect(callback) { const REDIS_HOST = config.get("REDIS_HOST"); @@ -36,7 +35,7 @@ function connect(callback) { const REDIS_PORT = config.get("REDIS_PORT"); const REDIS_DB = config.get("REDIS_DB"); redisClient = redis.createClient(REDIS_PORT, REDIS_HOST); - redisClient.select(REDIS_DB, function(err) { + redisClient.select(REDIS_DB, err => { exports.get = redisGet; exports.set = redisSet; exports.del = redisDel; @@ -44,191 +43,184 @@ function connect(callback) { exports.lock = redisLock; callback(err); }); - } - else { + } else { const mongodb = require("mongodb"); const MONGODB_CONNECTION_URL = config.get("MONGODB_CONNECTION_URL"); - mongodb.MongoClient.connect(MONGODB_CONNECTION_URL, function(err, db) { - if (err) - return callback(err); + mongodb.MongoClient.connect( + MONGODB_CONNECTION_URL, + (err, db) => { + if (err) return callback(err); - mongoDb = db; - mongoCollection = db.collection("cache"); - mongoCollection.ensureIndex({"expire": 1}, {expireAfterSeconds: 0}); + mongoDb = db; + mongoCollection = db.collection("cache"); + mongoCollection.ensureIndex({ expire: 1 }, { expireAfterSeconds: 0 }); - const now = Date.now(); - mongoDb.command({hostInfo: 1}, function(err, res) { - if (err) - return callback(err); - mongoTimeOffset = res.system.currentTime.getTime() - now; - exports.get = mongoGet; - exports.set = mongoSet; - exports.del = mongoDel; - exports.pop = mongoPop; - exports.lock = mongoLock; - callback(); - }); - }); + const now = Date.now(); + mongoDb.command({ hostInfo: 1 }, (err, res) => { + if (err) return callback(err); + mongoTimeOffset = res.system.currentTime.getTime() - now; + exports.get = mongoGet; + exports.set = mongoSet; + exports.del = mongoDel; + exports.pop = mongoPop; + exports.lock = mongoLock; + callback(); + }); + } + ); } } - function disconnect() { - if (redisClient) - redisClient.quit(); + if (redisClient) redisClient.quit(); - if (mongoDb) - mongoDb.close(); + if (mongoDb) mongoDb.close(); } - function redisGet(key, callback) { - if (Array.isArray(key)) - redisClient.mget(key, callback); - else - redisClient.get(key, callback); + if (Array.isArray(key)) redisClient.mget(key, callback); + else redisClient.get(key, callback); } - function redisDel(key, callback) { redisClient.del(key, callback); } - function redisSet(key, value, ttl, callback) { if (!callback && typeof v === "function") { callback = ttl; ttl = null; } - if (!ttl) - ttl = MAX_CACHE_TTL; + if (!ttl) ttl = MAX_CACHE_TTL; redisClient.setex(key, ttl, value, callback); } - function redisPop(key, callback) { - const POP_SCRIPT = "local v=redis.call(\"get\",KEYS[1]);redis.call(\"del\",KEYS[1]);return v;"; + const POP_SCRIPT = + 'local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;'; redisClient.eval(POP_SCRIPT, 1, key, callback); } - function redisLock(lockName, ttl, callback) { - const UNLOCK_SCRIPT = "if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"del\", KEYS[1]) else return 0 end"; - const EXTEND_SCRIPT = "if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"pexpire\", KEYS[1], ARGV[2]) else return 0 end"; - const token = Math.random().toString(36).slice(2); + const UNLOCK_SCRIPT = + 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'; + const EXTEND_SCRIPT = + 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end'; + const token = Math.random() + .toString(36) + .slice(2); - function unlockOrExtend(ttl) { - if (!ttl) { - redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, function(err, res) { - if (err || !res) - throw err || new Error("Lock expired"); - }); - } - else { - redisClient.eval(EXTEND_SCRIPT, 1, lockName, token, ttl, function(err, res) { - if (err || !res) - throw err || new Error("Lock expired"); + function unlockOrExtend(extendTtl) { + if (!extendTtl) { + redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, (err, res) => { + if (err || !res) throw err || new Error("Lock expired"); }); + } else { + redisClient.eval( + EXTEND_SCRIPT, + 1, + lockName, + token, + extendTtl, + (err, res) => { + if (err || !res) throw err || new Error("Lock expired"); + } + ); } } - redisClient.set(lockName, token, "NX", "EX", ttl, function(err, res) { - if (err || res) - return callback(err, unlockOrExtend); + redisClient.set(lockName, token, "NX", "EX", ttl, (err, res) => { + if (err || res) return callback(err, unlockOrExtend); - setTimeout(function() { + setTimeout(() => { redisLock(lockName, ttl, callback); }, 200); }); } - function mongoGet(key, callback) { let expire = new Date(Date.now() - mongoTimeOffset); - if (Array.isArray(key)) - mongoCollection.find({"_id": {"$in": key}}).toArray(function(err, res) { - if (err) - return callback(err); + if (Array.isArray(key)) { + mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => { + if (err) return callback(err); let indices = {}; - key.forEach(function(v, i) { + key.forEach((v, i) => { indices[v] = i; }); let values = []; - res.forEach(function(r) { - if (r["expire"] > expire) - values[indices[r["_id"]]] = r["value"]; + res.forEach(r => { + if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"]; }); callback(null, values); }); - else - mongoCollection.findOne({"_id": {"$in": [key]}}, function(err, res) { - if (err || !res) - return callback(err); + } else { + mongoCollection.findOne({ _id: { $in: [key] } }, (err, res) => { + if (err || !res) return callback(err); - if (res["expire"] > expire) - return callback(null, res["value"]); + if (res["expire"] > expire) return callback(null, res["value"]); callback(); }); + } } - function mongoDel(key, callback) { if (Array.isArray(key)) - mongoCollection.remove({"_id": {"$in": key}}, callback); - else - mongoCollection.remove({"_id": key}, callback); + mongoCollection.remove({ _id: { $in: key } }, callback); + else mongoCollection.remove({ _id: key }, callback); } - function mongoSet(key, value, ttl, callback) { if (!callback && typeof v === "function") { callback = ttl; ttl = null; } - if (!ttl) - ttl = MAX_CACHE_TTL; + if (!ttl) ttl = MAX_CACHE_TTL; - let expire = new Date((Date.now() - mongoTimeOffset) + (ttl * 1000)); - mongoCollection.save({"_id": key, "value": value, "expire": expire}, callback); + let expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000); + mongoCollection.save({ _id: key, value: value, expire: expire }, callback); } - function mongoPop(key, callback) { - mongoCollection.findAndModify({"_id": key}, null, null, {remove: true}, function(err, res) { - if (err || !res["value"]) - return callback(err); + mongoCollection.findAndModify( + { _id: key }, + null, + null, + { remove: true }, + (err, res) => { + if (err || !res["value"]) return callback(err); - if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset)) - return callback(null, res["value"]["value"]); + if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset)) + return callback(null, res["value"]["value"]); - callback(); - }); + callback(); + } + ); } - function mongoLock(lockName, ttl, callback) { - const token = Math.random().toString(36).slice(2); + const token = Math.random() + .toString(36) + .slice(2); - function unlockOrExtend(ttl) { - if (!ttl) { - mongoCollection.remove({"_id": lockName, "value": token}, function(err, res) { + function unlockOrExtend(extendTtl) { + if (!extendTtl) { + mongoCollection.remove({ _id: lockName, value: token }, (err, res) => { if (err || res["result"]["n"] !== 1) throw err || new Error("Lock expired"); }); - } - else { - let expire = new Date((Date.now() - mongoTimeOffset) + (ttl * 1000)); + } else { + let expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000); mongoCollection.update( - {"_id": lockName, "value": token}, - {"expire": expire}, - function(err, res) { + { _id: lockName, value: token }, + { expire: expire }, + (err, res) => { if (err || res["result"]["n"] !== 1) throw err | new Error("Lock expired"); } @@ -237,21 +229,23 @@ function mongoLock(lockName, ttl, callback) { } let expireTest = new Date(Date.now() - mongoTimeOffset); - let expireSet = new Date((Date.now() - mongoTimeOffset) + (ttl * 1000)); + let expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000); mongoCollection.update( - {"_id": lockName, "expire": {"$lte": expireTest}}, - {"$set": {"value" : token, "expire": expireSet}}, - {upsert: true}, - function(err, res) { - if (err && err.code === 11000) - return setTimeout(function() {mongoLock(lockName, ttl, callback)}, 200); + { _id: lockName, expire: { $lte: expireTest } }, + { $set: { value: token, expire: expireSet } }, + { upsert: true }, + err => { + if (err && err.code === 11000) { + return setTimeout(() => { + mongoLock(lockName, ttl, callback); + }, 200); + } return callback(err, unlockOrExtend); } ); } - exports.connect = connect; exports.disconnect = disconnect; diff --git a/lib/cluster.js b/lib/cluster.js index 5ce19fc..6410465 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -30,43 +30,43 @@ let tooManyCrashesTimestamp = 0; function getDependencyVersions() { let dependencies = [`node@${process.versions.node}`]; try { - let npmls = JSON.parse(require("child_process").execSync("npm ls --prod --json --depth 0")); + let npmls = JSON.parse( + require("child_process").execSync("npm ls --prod --json --depth 0") + ); for (let k in npmls.dependencies) dependencies.push(`${k}@${npmls.dependencies[k].version}`); - } catch (err) {} + } catch (err) { + // Continue regardless of error + } return dependencies.join(","); } - function getConfig() { let conf = []; const keys = Object.keys(process.env); - for (let k of keys) + for (let k of keys) { if (k.startsWith("GENIEACS_")) { const name = k.slice(9); const value = process.env[k]; const def = config.getDefault(name); - if (def == null || value !== `${def}`) - conf.push(`${name}=${value}`); + if (def == null || value !== `${def}`) conf.push(`${name}=${value}`); } + } return conf.join(","); } - function restartWorker(worker, code, signal) { let msg = { message: "Worker died", pid: worker.process.pid }; - if (code != null) - msg.exitCode = code; + if (code != null) msg.exitCode = code; - if (signal != null) - msg.signal = signal; + if (signal != null) msg.signal = signal; logger.error(msg); @@ -75,14 +75,13 @@ function restartWorker(worker, code, signal) { if (respawnTimestamp === now) { tooManyCrashesTimestamp = now; cluster.fork(); - return + return; } if (now - tooManyCrashesTimestamp > 60000) { process.exitCode = 1; cluster.removeListener("exit", restartWorker); - for (let pid in cluster.workers) - cluster.workers[pid].kill(); + for (let pid in cluster.workers) cluster.workers[pid].kill(); logger.error({ message: "Too many crashes, exiting", @@ -91,18 +90,16 @@ function restartWorker(worker, code, signal) { return; } - setTimeout(function() { + setTimeout(() => { if (process.exitCode) return; cluster.fork(); }, respawnTimestamp - now); } - function start(service) { logger.init(service, VERSION); - const config = require("./config"); - const SERVICE_ADDRESS = config.get(`${service.toUpperCase()}_INTERFACE`) - const SERVICE_PORT = config.get(`${service.toUpperCase()}_PORT`) + const SERVICE_ADDRESS = config.get(`${service.toUpperCase()}_INTERFACE`); + const SERVICE_PORT = config.get(`${service.toUpperCase()}_PORT`); logger.info({ message: `GenieACS (genieacs-${service}) starting`, @@ -112,9 +109,12 @@ function start(service) { config: getConfig() }); - cluster.on("listening", function(worker, address) { - if ((address.addressType === 4 || address.addressType === 6) && - address.address === SERVICE_ADDRESS && address.port === SERVICE_PORT) { + cluster.on("listening", (worker, address) => { + if ( + (address.addressType === 4 || address.addressType === 6) && + address.address === SERVICE_ADDRESS && + address.port === SERVICE_PORT + ) { logger.info({ message: "Worker listening", pid: worker.process.pid, @@ -126,7 +126,7 @@ function start(service) { cluster.on("exit", restartWorker); - process.on("SIGINT", function() { + process.on("SIGINT", () => { logger.info({ message: "Received signal SIGINT, exiting", pid: process.pid @@ -135,15 +135,14 @@ function start(service) { cluster.removeListener("exit", restartWorker); }); - process.on("SIGTERM", function() { + process.on("SIGTERM", () => { logger.info({ message: "Received signal SIGTERM, exiting", pid: process.pid }); cluster.removeListener("exit", restartWorker); - for (let pid in cluster.workers) - cluster.workers[pid].kill(); + for (let pid in cluster.workers) cluster.workers[pid].kill(); }); cluster.setupMaster({ @@ -153,15 +152,12 @@ function start(service) { let workerCount = config.get(`${service.toUpperCase()}_WORKER_PROCESSES`); - if (!workerCount) - workerCount = Math.max(2, require("os").cpus().length); + if (!workerCount) workerCount = Math.max(2, require("os").cpus().length); - for (let i = 0; i < workerCount; ++ i) - cluster.fork(); + for (let i = 0; i < workerCount; ++i) cluster.fork(); } - -process.on("uncaughtException", function(err) { +process.on("uncaughtException", err => { logger.error({ message: "Uncaught exception in master process, exiting", exception: err, @@ -169,9 +165,7 @@ process.on("uncaughtException", function(err) { }); process.exitCode = 1; cluster.removeListener("exit", restartWorker); - for (let pid in cluster.workers) - cluster.workers[pid].kill(); + for (let pid in cluster.workers) cluster.workers[pid].kill(); }); - exports.start = start; diff --git a/lib/common.js b/lib/common.js index 05f9e96..6b9db82 100644 --- a/lib/common.js +++ b/lib/common.js @@ -28,24 +28,30 @@ const OBJECT_TYPE = "[object Object]"; const REGEXP_TYPE = "[object RegExp]"; const DATE_TYPE = "[object Date]"; -const typeOf = (obj) => Object.prototype.toString.call(obj); +const typeOf = obj => Object.prototype.toString.call(obj); function generateDeviceId(deviceIdStruct) { // Escapes everything except alphanumerics and underscore function esc(str) { - return str.replace(/[^A-Za-z0-9_]/g, (chr) => { + return str.replace(/[^A-Za-z0-9_]/g, chr => { const buf = new Buffer(chr, "utf8"); let rep = ""; - for (let b of buf) - rep += "%" + (b.toString(16).toUpperCase()); + for (let b of buf) rep += "%" + b.toString(16).toUpperCase(); return rep; }); } // Guaranteeing globally unique id as defined in TR-069 - if (deviceIdStruct["ProductClass"]) - return (esc(deviceIdStruct["OUI"])) + "-" + (esc(deviceIdStruct["ProductClass"])) + "-" + (esc(deviceIdStruct["SerialNumber"])); - return (esc(deviceIdStruct["OUI"])) + "-" + (esc(deviceIdStruct["SerialNumber"])); + if (deviceIdStruct["ProductClass"]) { + return ( + esc(deviceIdStruct["OUI"]) + + "-" + + esc(deviceIdStruct["ProductClass"]) + + "-" + + esc(deviceIdStruct["SerialNumber"]) + ); + } + return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]); } function parseAlias(pattern, start, res) { @@ -53,22 +59,21 @@ function parseAlias(pattern, start, res) { let i = start; while (i < pattern.length && pattern[i] !== "]") { const alias = []; - let j = i = parsePath(pattern, i, alias) + 1; + let j = (i = parsePath(pattern, i, alias) + 1); while (pattern[j] !== "]" && pattern[j] !== ",") { - if (pattern[j] === "\"" && i === j) { + if (pattern[j] === '"' && i === j) { ++j; - while (pattern[j] !== "\"" || pattern[j - 1] === "\\") { + while (pattern[j] !== '"' || pattern[j - 1] === "\\") { if (++j >= pattern.length) throw new Error("Invalid alias expression"); } } - if (++j >= pattern.length) - throw new Error("Invalid alias expression"); + if (++j >= pattern.length) throw new Error("Invalid alias expression"); } let value = pattern.slice(i, j).trim(); i = j; - if (value[0] === "\"") { + if (value[0] === '"') { try { value = JSON.parse(value); } catch (error) { @@ -78,8 +83,7 @@ function parseAlias(pattern, start, res) { alias.push(value); aliases.push(alias); - if (pattern[i] === ",") - ++i; + if (pattern[i] === ",") ++i; } // Sort to ensure identical expressions have idential string representation @@ -87,20 +91,15 @@ function parseAlias(pattern, start, res) { const jMax = Math.min(a.length, b.length); for (let j = 0; j < jMax; j += 2) { const kMax = Math.min(a[j].length, b[j].length); - for (let k = 0; k < kMax; ++ k) { + for (let k = 0; k < kMax; ++k) { if (Array.isArray(a[j][k])) { - if (Array.isArray(b[j][k])) - return srt(a[j][k], b[j][k]); - else if (b[j][k] == null) - return -1; - else - return 1; + if (Array.isArray(b[j][k])) return srt(a[j][k], b[j][k]); + else if (b[j][k] == null) return -1; + else return 1; } else if (a[j][k] == null) { - if (b[j][k] == null) - return 0; - else - return 1; - } else if ((b[j][k] == null) || Array.isArray(b[j][k])) { + if (b[j][k] == null) return 0; + else return 1; + } else if (b[j][k] == null || Array.isArray(b[j][k])) { return -1; } else if (a[j][k] > b[j][k]) { return 1; @@ -109,21 +108,15 @@ function parseAlias(pattern, start, res) { } } - if (a[j].length > b[j].length) - return -1; - else if (a[j].length < b[j].length) - return 1; + if (a[j].length > b[j].length) return -1; + else if (a[j].length < b[j].length) return 1; - if (a[j + 1] > b[j + 1]) - return -1; - else if (a[j + 1] < b[j + 1]) - return 1; + if (a[j + 1] > b[j + 1]) return -1; + else if (a[j + 1] < b[j + 1]) return 1; } - if (a.length > b.length) - return -1; - else if (a.length < b.length) - return 1; + if (a.length > b.length) return -1; + else if (a.length < b.length) return 1; return 0; } @@ -141,7 +134,7 @@ function parsePath(pattern, start, res) { // Colon separator is needed for parseAlias if (i < pattern.length && pattern[i] !== ":") { - while (true) { + for (;;) { if (pattern[i] === "[") { path.alias |= 1 << path.length; i = parseAlias(pattern, i + 1, path) + 1; @@ -151,15 +144,12 @@ function parsePath(pattern, start, res) { ++i; const n = pattern.slice(j, i); - if (n === "*") - path.wildcard |= 1 << path.length; + if (n === "*") path.wildcard |= 1 << path.length; path.push(n); } - if (i >= pattern.length || pattern[i] === ":") - break; - else if (pattern[i] !== ".") - throw new Error("Invalid alias expression"); + if (i >= pattern.length || pattern[i] === ":") break; + else if (pattern[i] !== ".") throw new Error("Invalid alias expression"); ++i; } @@ -167,16 +157,14 @@ function parsePath(pattern, start, res) { Object.freeze(path); - if (res == null) - return path; + if (res == null) return path; res.push(path); return i; } function addPathMeta(path) { - if (path.alias != null && path.wildcard != null) - return path; + if (path.alias != null && path.wildcard != null) return path; path.alias = 0; path.wildcard = 0; @@ -184,8 +172,7 @@ function addPathMeta(path) { for (let [i, p] of path.entries()) { if (Array.isArray(p)) { path.alias |= 1 << i; - for (let j = 0; j < p.length; j += 2) - addPathMeta(p[j]); + for (let j = 0; j < p.length; j += 2) addPathMeta(p[j]); } else if (p === "*") { path.wildcard |= 1 << i; } @@ -197,7 +184,7 @@ function addPathMeta(path) { // Source: http://stackoverflow.com/a/6969486 function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); } exports.UNDEFINED_TYPE = UNDEFINED_TYPE; diff --git a/lib/config.js b/lib/config.js index 6c59021..560357d 100644 --- a/lib/config.js +++ b/lib/config.js @@ -21,81 +21,89 @@ const path = require("path"); const options = { - CONFIG_DIR: {type: "path", "default": "config"}, - MONGODB_CONNECTION_URL: {type: "string", "default": "mongodb://127.0.0.1/genieacs"}, - REDIS_PORT: {type: "int", "default": 6379}, - REDIS_HOST: {type: "string", "default": ""}, - REDIS_DB: {type: "int", "default": 0}, + CONFIG_DIR: { type: "path", default: "config" }, + MONGODB_CONNECTION_URL: { + type: "string", + default: "mongodb://127.0.0.1/genieacs" + }, + REDIS_PORT: { type: "int", default: 6379 }, + REDIS_HOST: { type: "string", default: "" }, + REDIS_DB: { type: "int", default: 0 }, - CWMP_WORKER_PROCESSES: {type: "int", "default": 0}, - CWMP_PORT: {type: "int", "default": 7547}, - CWMP_INTERFACE: {type: "string", "default": "0.0.0.0"}, - CWMP_SSL: {type: "bool", "default": false}, - CWMP_LOG_FILE: {type: "path", "default": ""}, - CWMP_ACCESS_LOG_FILE: {type: "path", "default": ""}, + CWMP_WORKER_PROCESSES: { type: "int", default: 0 }, + CWMP_PORT: { type: "int", default: 7547 }, + CWMP_INTERFACE: { type: "string", default: "0.0.0.0" }, + CWMP_SSL: { type: "bool", default: false }, + CWMP_LOG_FILE: { type: "path", default: "" }, + CWMP_ACCESS_LOG_FILE: { type: "path", default: "" }, - NBI_WORKER_PROCESSES: {type: "int", "default": 0}, - NBI_PORT: {type: "int", "default": 7557}, - NBI_INTERFACE: {type: "string", "default": "0.0.0.0"}, - NBI_SSL: {type: "bool", "default": false}, - NBI_LOG_FILE: {type: "path", "default": ""}, - NBI_ACCESS_LOG_FILE: {type: "path", "default": ""}, + NBI_WORKER_PROCESSES: { type: "int", default: 0 }, + NBI_PORT: { type: "int", default: 7557 }, + NBI_INTERFACE: { type: "string", default: "0.0.0.0" }, + NBI_SSL: { type: "bool", default: false }, + NBI_LOG_FILE: { type: "path", default: "" }, + NBI_ACCESS_LOG_FILE: { type: "path", default: "" }, - FS_WORKER_PROCESSES: {type: "int", "default": 0}, - FS_PORT: {type: "int", "default": 7567}, - FS_INTERFACE: {type: "string", "default": "0.0.0.0"}, - FS_SSL: {type: "bool", "default": false}, - FS_HOSTNAME: {type: "string", "default": "acs.example.com"}, - FS_LOG_FILE: {type: "path", "default": ""}, - FS_ACCESS_LOG_FILE: {type: "path", "default": ""}, + FS_WORKER_PROCESSES: { type: "int", default: 0 }, + FS_PORT: { type: "int", default: 7567 }, + FS_INTERFACE: { type: "string", default: "0.0.0.0" }, + FS_SSL: { type: "bool", default: false }, + FS_HOSTNAME: { type: "string", default: "acs.example.com" }, + FS_LOG_FILE: { type: "path", default: "" }, + FS_ACCESS_LOG_FILE: { type: "path", default: "" }, - UDP_CONNECTION_REQUEST_PORT: {type: "int", "default": 0}, + UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 }, - DOWNLOAD_TIMEOUT: {type: "int", "default": 3600}, - EXT_TIMEOUT: {type: "int", "default": 3000}, - MAX_CACHE_TTL: {type: "int", "default": 86400}, - DEBUG: {type: "bool", "default": false}, - RETRY_DELAY: {type: "int", "default": 300}, - SESSION_TIMEOUT: {type: "int", "default": 30}, - CONNECTION_REQUEST_TIMEOUT: {type: "int", "default": 2000}, - GPN_NEXT_LEVEL: {type: "int", "default": 0}, - GPV_BATCH_SIZE: {type: "int", "default": 32}, - MAX_DEPTH: {type: "int", "default": 16}, - COOKIES_PATH: {type: "string"}, - LOG_FORMAT: {type: "string", "default": "simple"}, - ACCESS_LOG_FORMAT: {type: "string", "default": ""}, - MAX_CONCURRENT_REQUESTS: {type: "int", "default": 20}, - DATETIME_MILLISECONDS: {type: "bool", "default": true}, - BOOLEAN_LITERAL: {type: "bool", "default": true}, - CONNECTION_REQUEST_ALLOW_BASIC_AUTH: {type: "bool", "default": false}, - MAX_COMMIT_ITERATIONS: {type: "int", "default": 32}, + DOWNLOAD_TIMEOUT: { type: "int", default: 3600 }, + EXT_TIMEOUT: { type: "int", default: 3000 }, + MAX_CACHE_TTL: { type: "int", default: 86400 }, + DEBUG: { type: "bool", default: false }, + RETRY_DELAY: { type: "int", default: 300 }, + SESSION_TIMEOUT: { type: "int", default: 30 }, + CONNECTION_REQUEST_TIMEOUT: { type: "int", default: 2000 }, + GPN_NEXT_LEVEL: { type: "int", default: 0 }, + GPV_BATCH_SIZE: { type: "int", default: 32 }, + MAX_DEPTH: { type: "int", default: 16 }, + COOKIES_PATH: { type: "string" }, + LOG_FORMAT: { type: "string", default: "simple" }, + ACCESS_LOG_FORMAT: { type: "string", default: "" }, + MAX_CONCURRENT_REQUESTS: { type: "int", default: 20 }, + DATETIME_MILLISECONDS: { type: "bool", default: true }, + BOOLEAN_LITERAL: { type: "bool", default: true }, + CONNECTION_REQUEST_ALLOW_BASIC_AUTH: { type: "bool", default: false }, + MAX_COMMIT_ITERATIONS: { type: "int", default: 32 }, // XML configuration - XML_RECOVER: {type: "bool", "default": false}, - XML_IGNORE_ENC: {type: "bool", "default": false}, - XML_FORMAT: {type: "bool", "default": false}, - XML_NO_DECL: {type: "bool", "default": false}, - XML_NO_EMPTY: {type: "bool", "default": false}, - XML_IGNORE_NAMESPACE: {type: "bool", "default": false}, + XML_RECOVER: { type: "bool", default: false }, + XML_IGNORE_ENC: { type: "bool", default: false }, + XML_FORMAT: { type: "bool", default: false }, + XML_NO_DECL: { type: "bool", default: false }, + XML_NO_EMPTY: { type: "bool", default: false }, + XML_IGNORE_NAMESPACE: { type: "bool", default: false }, // Should probably never be changed - DEVICE_ONLINE_THRESHOLD: {type: "int", "default": 4000} + DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 } }; const allConfig = {}; function setConfig(name, value, commandLineArgument) { - if (allConfig[name] != null) - return true; + if (allConfig[name] != null) return true; // For compatibility with v1.0 if (name === "PRESETS_CACHE_DURATION" || name === "presets-cache-duration") setConfig("MAX_CACHE_TTL", value); - if (name === "GET_PARAMETER_NAMES_DEPTH_THRESHOLD" || name === "get-parameter-names-depth-threshold") + if ( + name === "GET_PARAMETER_NAMES_DEPTH_THRESHOLD" || + name === "get-parameter-names-depth-threshold" + ) setConfig("GPN_NEXT_LEVEL", value); - if (name === "TASK_PARAMETERS_BATCH_SIZE" || name === "task-parameters-batch-size") + if ( + name === "TASK_PARAMETERS_BATCH_SIZE" || + name === "task-parameters-batch-size" + ) setConfig("GPV_BATCH_SIZE", value); if (name === "IGNORE_XML_NAMESPACES" || name === "ignore-xml-namespaces") @@ -107,20 +115,22 @@ function setConfig(name, value, commandLineArgument) { if (name === "XML_PARSE_RECOVER" || name === "xml-parse-recover") setConfig("XML_RECOVER", value); - if (name === "FS_IP" || name === "fs-ip") - setConfig("FS_HOSTNAME", value); + if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value); function cast(val, type) { switch (type) { case "int": return Number(val); case "bool": - return ["true", "on", "yes", "1"].includes(String(val).trim().toLowerCase()); + return ["true", "on", "yes", "1"].includes( + String(val) + .trim() + .toLowerCase() + ); case "string": return String(val); case "path": - if (!val) - return ""; + if (!val) return ""; return path.resolve(val); default: return null; @@ -130,8 +140,7 @@ function setConfig(name, value, commandLineArgument) { let _value = null; for (let [optionName, optionDetails] of Object.entries(options)) { let n = optionName; - if (commandLineArgument) - n = n.toLowerCase().replace(/_/g, "-"); + if (commandLineArgument) n = n.toLowerCase().replace(/_/g, "-"); if (name === n) { _value = cast(value, optionDetails.type); @@ -163,55 +172,47 @@ while (argv.length) { } // Environment variable -for (const [k, v] of Object.entries(process.env)) { - if (k.startsWith("GENIEACS_")) - setConfig(k.slice(9), v); -} +for (const [k, v] of Object.entries(process.env)) + if (k.startsWith("GENIEACS_")) setConfig(k.slice(9), v); // Use default config dir if none defined setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]); // Configuration file const configFile = require(path.resolve(allConfig.CONFIG_DIR, "config")); -for (const [k, v] of Object.entries(configFile)) +for (const [k, v] of Object.entries(configFile)) { if (!setConfig(k, v)) // Pass as environment variable to be accessable by extensions process.env[`GENIEACS_${k}`] = `${v}`; +} // Defaults for (const [k, v] of Object.entries(options)) - if (v["default"] != null) - setConfig(k, v["default"]); + if (v["default"] != null) setConfig(k, v["default"]); function get(optionName, deviceId) { - if (!deviceId) - return allConfig[optionName]; + if (!deviceId) return allConfig[optionName]; optionName = `${optionName}-${deviceId}`; let v = allConfig[optionName]; - if (v != null) - return v; + if (v != null) return v; let i = optionName.lastIndexOf("-"); v = allConfig[optionName.slice(0, i)]; - if (v != null) - return v; + if (v != null) return v; i = optionName.lastIndexOf("-", i - 1); v = allConfig[optionName.slice(0, i)]; - if (v != null) - return v; + if (v != null) return v; i = optionName.lastIndexOf("-", i - 1); v = allConfig[optionName.slice(0, i)]; - if (v != null) - return v; + if (v != null) return v; i = optionName.lastIndexOf("-", i - 1); if (i > 0) { v = allConfig[optionName.slice(0, i)]; - if (v != null) - return v; + if (v != null) return v; } return null; @@ -219,12 +220,10 @@ function get(optionName, deviceId) { function getDefault(optionName) { const option = options[optionName]; - if (!option) - return null; + if (!option) return null; let val = option["default"]; - if (val && option.type === "path") - val = path.resolve(val); + if (val && option.type === "path") val = path.resolve(val); return val; } @@ -232,7 +231,9 @@ function getDefault(optionName) { // Load authentication scripts try { exports.auth = require(path.resolve(allConfig.CONFIG_DIR, "auth")); -} catch (error) {} +} catch (error) { + // No auth.js exists +} exports.get = get; exports.getDefault = getDefault; diff --git a/lib/default-provisions.js b/lib/default-provisions.js index 1559380..7c2edb3 100644 --- a/lib/default-provisions.js +++ b/lib/default-provisions.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -25,8 +25,7 @@ const scheduling = require("./scheduling"); const MAX_DEPTH = config.get("MAX_DEPTH"); - -function refresh(sessionContext, provision, declarations, startRevision, endRevision) { +function refresh(sessionContext, provision, declarations) { let path = common.parsePath(provision[1]).slice(); let l = path.length; path.length = MAX_DEPTH; @@ -35,70 +34,100 @@ function refresh(sessionContext, provision, declarations, startRevision, endRevi let offset = scheduling.variance(sessionContext.deviceId, every); let t = scheduling.interval(sessionContext.timestamp, every, offset); - for (let i = l; i < path.length; ++ i) - declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]); + for (let i = l; i < path.length; ++i) { + declarations.push([ + path.slice(0, i), + t, + { object: 1, writable: 1, value: t } + ]); + } return true; } - -function value(sessionContext, provision, declarations, startRevision, endRevision) { +function value(sessionContext, provision, declarations) { let path = common.parsePath(provision[1]); - declarations.push([path, 1, {value: 1}, null, {value: [provision[2]]}]); + declarations.push([path, 1, { value: 1 }, null, { value: [provision[2]] }]); return true; } - -function tag(sessionContext, provision, declarations, startRevision, endRevision) { +function tag(sessionContext, provision, declarations) { let path = ["Tags", provision[1]]; - declarations.push([path, 1, {value: 1}, null, {value: [provision[2]]}]); + declarations.push([path, 1, { value: 1 }, null, { value: [provision[2]] }]); return true; } - -function reboot(sessionContext, provision, declarations, startRevision, endRevision) { - declarations.push([["Reboot"], 1, {value: 1}, null, {value: [sessionContext.timestamp]}]); +function reboot(sessionContext, provision, declarations) { + declarations.push([ + ["Reboot"], + 1, + { value: 1 }, + null, + { value: [sessionContext.timestamp] } + ]); return true; } - -function reset(sessionContext, provision, declarations, startRevision, endRevision) { - declarations.push([["FactoryReset"], 1, {value: 1}, null, {value: [sessionContext.timestamp]}]); +function reset(sessionContext, provision, declarations) { + declarations.push([ + ["FactoryReset"], + 1, + { value: 1 }, + null, + { value: [sessionContext.timestamp] } + ]); return true; } - -function download(sessionContext, provision, declarations, startRevision, endRevision) { - let alias = [["FileType"], provision[1] || "", ["FileName"], provision[2] || "", - ["TargetFileName"], provision[3] || ""]; +function download(sessionContext, provision, declarations) { + let alias = [ + ["FileType"], + provision[1] || "", + ["FileName"], + provision[2] || "", + ["TargetFileName"], + provision[3] || "" + ]; declarations.push([["Downloads", alias], 1, {}, 1]); - declarations.push([["Downloads", alias, "Download"], 1, {value: 1}, null, - {value: [sessionContext.timestamp]}]); + declarations.push([ + ["Downloads", alias, "Download"], + 1, + { value: 1 }, + null, + { value: [sessionContext.timestamp] } + ]); return true; } - -function instances(sessionContext, provision, declarations, startRevision, endRevision) { +function instances( + sessionContext, + provision, + declarations, + startRevision, + endRevision +) { let count = Number(provision[2]); - if (Number.isNaN(count)) - return true; + if (Number.isNaN(count)) return true; let path = common.parsePath(provision[1]); if (provision[2][0] === "+" || provision[2][0] === "-") { declarations.push([path, 1]); - if (endRevision === startRevision) - return false; + if (endRevision === startRevision) return false; - let unpacked = device.unpack(sessionContext.deviceData, path, startRevision + 1); + let unpacked = device.unpack( + sessionContext.deviceData, + path, + startRevision + 1 + ); count = Math.max(0, unpacked.length + count); } @@ -107,7 +136,6 @@ function instances(sessionContext, provision, declarations, startRevision, endRe return true; } - exports.refresh = refresh; exports.value = value; exports.tag = tag; diff --git a/lib/device.js b/lib/device.js index d70a7f9..3cb3213 100644 --- a/lib/device.js +++ b/lib/device.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -18,97 +18,88 @@ */ "use strict"; -const common = require('./common'); - - const CHANGE_FLAGS = { object: 2, writable: 4, value: 8 }; - function parseBool(v) { v = "" + v; - if (v === "true" || v === "TRUE" || v === "True" || v === "1") - return true; + if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true; else if (v === "false" || v === "FALSE" || v === "False" || v === "0") return false; - else - return null; + else return null; } - function sanitizeParameterValue(parameterValue) { if (parameterValue[0] != null) { switch (parameterValue[1]) { - case 'xsd:boolean': - if (typeof parameterValue[0] !== 'boolean') { + case "xsd:boolean": + if (typeof parameterValue[0] !== "boolean") { let v = parseBool(parameterValue[0]); - if (v == null) - v = '' + parameterValue[0]; + if (v == null) v = "" + parameterValue[0]; parameterValue[0] = v; } break; - case 'xsd:string': - case 'xsd:base64': - case 'xsd:hexBinary': - if (typeof parameterValue[0] !== 'string') { - parameterValue[0] = '' + parameterValue[0]; - } + case "xsd:string": + case "xsd:base64": + case "xsd:hexBinary": + if (typeof parameterValue[0] !== "string") + parameterValue[0] = "" + parameterValue[0]; + break; - case 'xsd:int': - case 'xsd:unsignedInt': - if (typeof parameterValue[0] !== 'number') { + case "xsd:int": + case "xsd:unsignedInt": + if (typeof parameterValue[0] !== "number") { let v = parseInt(parameterValue[0]); - if (isNaN(v)) - v = '' + parameterValue[0]; + if (isNaN(v)) v = "" + parameterValue[0]; parameterValue[0] = v; } break; - case 'xsd:dateTime': - if (typeof parameterValue[0] !== 'number') { + case "xsd:dateTime": + if (typeof parameterValue[0] !== "number") { // Don't use parseInt because it reads date string as a number let v = +parameterValue[0]; if (isNaN(v)) { v = Date.parse(parameterValue[0]); - if (isNaN(v)) - v = '' + parameterValue[0]; + if (isNaN(v)) v = "" + parameterValue[0]; } parameterValue[0] = v; } break; default: - if (parameterValue[1] != null) - throw new Error(`Parameter value type "${parameterValue[1]}" not recognized.`); + if (parameterValue[1] != null) { + throw new Error( + `Parameter value type "${parameterValue[1]}" not recognized.` + ); + } parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0])); break; } } + return parameterValue; } - function getAliasDeclarations(path, timestamp) { var decs = []; - function recursive(pattern, prefix) { - var pattern = prefix.concat(pattern); + function recursive(p, prefix) { + const pattern = prefix.concat(p); - for (let i = prefix.length; i < pattern.length; ++ i) { + for (let i = prefix.length; i < pattern.length; ++i) { if (Array.isArray(pattern[i])) { let pat = pattern[i]; - pattern[i] = '*'; + pattern[i] = "*"; for (let j = 0; j < pat.length; j += 2) recursive(pat[j], pattern.slice(0, i + 1)); } } - if (prefix.length) - decs.push([pattern, timestamp, {value: timestamp}]); - else - decs.push([pattern, timestamp]); + if (prefix.length) decs.push([pattern, timestamp, { value: timestamp }]); + else decs.push([pattern, timestamp]); } recursive(path, []); @@ -116,44 +107,51 @@ function getAliasDeclarations(path, timestamp) { return decs; } - function unpack(deviceData, path, revision) { let allMatches = []; if (!path.alias) { for (let p of deviceData.paths.find(path, false, true)) - if (deviceData.attributes.has(p, revision)) - allMatches.push(p); - } - else { + if (deviceData.attributes.has(p, revision)) allMatches.push(p); + } else { let wildcardPath = path.slice(); wildcardPath.wildcard = path.wildcard; - for (let i = 0; i < wildcardPath.length; ++ i) + for (let i = 0; i < wildcardPath.length; ++i) { if (Array.isArray(wildcardPath[i])) { - wildcardPath[i] = '*'; + wildcardPath[i] = "*"; wildcardPath.wildcard |= 1 << i; } + } for (let p of deviceData.paths.find(wildcardPath, false, true)) - if (deviceData.attributes.has(p, revision)) - allMatches.push(p); + if (deviceData.attributes.has(p, revision)) allMatches.push(p); - for (let i = path.length - 1; i >= 0; -- i) { + for (let i = path.length - 1; i >= 0; --i) { if (Array.isArray(path[i])) { for (let j = 0; j < path[i].length; j += 2) { let p = wildcardPath.slice(0, i + 1).concat(path[i][j]); - p.alias = (wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias << (i + 1); - p.wildcard = (wildcardPath.wildcard & ((1 << (i + 1)) - 1)) + path[i][j].wildcard << (i + 1); + p.alias = + ((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) << + (i + 1); + p.wildcard = + ((wildcardPath.wildcard & ((1 << (i + 1)) - 1)) + + path[i][j].wildcard) << + (i + 1); let unpacked = unpack(deviceData, p, revision); let filtered = []; for (let up of unpacked) { let attributes = deviceData.attributes.get(up, revision); - if (attributes && attributes.value && attributes.value[1] && - sanitizeParameterValue([path[i][j + 1], attributes.value[1][1]])[0] === attributes.value[1][0]) { - for (let m = 0; m < allMatches.length; ++ m) { + if ( + attributes && + attributes.value && + attributes.value[1] && + sanitizeParameterValue([ + path[i][j + 1], + attributes.value[1][1] + ])[0] === attributes.value[1][0] + ) { + for (let m = 0; m < allMatches.length; ++m) { let k; - for (k = i; k >= 0; -- k) - if (allMatches[m][k] != up[k]) - break + for (k = i; k >= 0; --k) if (allMatches[m][k] != up[k]) break; if (k < 0) { filtered.push(allMatches[m]); @@ -168,22 +166,20 @@ function unpack(deviceData, path, revision) { } } - allMatches.sort(function(a, b) { - for (let i = 0; i < a.length; ++ i) { - if (a === b) - continue; + allMatches.sort((a, b) => { + for (let i = 0; i < a.length; ++i) { + if (a === b) continue; // Use numeric sorting for numbers - let ia = parseInt(a), ib = parseInt(b); + let ia = parseInt(a), + ib = parseInt(b); if (ia.toString() === a && ib.toString() === b) { a = ia; b = ib; } - if (a < b) - return -1; - else - return 1; + if (a < b) return -1; + else return 1; } return 0; }); @@ -191,7 +187,6 @@ function unpack(deviceData, path, revision) { return allMatches; } - function clear(deviceData, path, timestamp, attributes, changeFlags = 0) { let changeTrackers = {}; @@ -205,28 +200,34 @@ function clear(deviceData, path, timestamp, attributes, changeFlags = 0) { attributes.value = attributes.object; } - for (let p of deviceData.paths.find(path, true, true, descendantsTimestamp ? 99 : path.length)) { + for (let p of deviceData.paths.find( + path, + true, + true, + descendantsTimestamp ? 99 : path.length + )) { let tracker = deviceData.trackers.get(p); - for (let k in tracker) - changeTrackers[k] |= tracker[k]; + for (let k in tracker) changeTrackers[k] |= tracker[k]; let currentTimestamp = deviceData.timestamps.get(p); - if (currentTimestamp === undefined) - continue; + if (currentTimestamp === undefined) continue; - if (timestamp > currentTimestamp || - (descendantsTimestamp > currentTimestamp && p.length > path.length)) { + if ( + timestamp > currentTimestamp || + (descendantsTimestamp > currentTimestamp && p.length > path.length) + ) { deviceData.timestamps.delete(p); deviceData.attributes.delete(p); changeFlags |= 1; - } - else if (attributes && p.length == path.length) { + } else if (attributes && p.length == path.length) { let currentAttributes = deviceData.attributes.get(p); if (currentAttributes) { let newAttrs; for (let attrName in attributes) { - if (attrName in currentAttributes && - attributes[attrName] > currentAttributes[attrName][0]) { + if ( + attrName in currentAttributes && + attributes[attrName] > currentAttributes[attrName][0] + ) { changeFlags |= CHANGE_FLAGS[attrName]; if (!newAttrs) { newAttrs = Object.assign({}, currentAttributes); @@ -241,27 +242,25 @@ function clear(deviceData, path, timestamp, attributes, changeFlags = 0) { // Note: For performance, we're merging all changes together rather than // mark changes based the exact parameters affected. - for (let k in changeTrackers) { - if (changeTrackers[k] & changeFlags) - deviceData.changes.add(k); - } + for (let k in changeTrackers) + if (changeTrackers[k] & changeFlags) deviceData.changes.add(k); } - function compareEquality(a, b) { var t = typeof a; - if (a === null || - a === undefined || - t === 'number' || - t === 'boolean' || - t === 'string' || - t === 'symbol') + if ( + a === null || + a === undefined || + t === "number" || + t === "boolean" || + t === "string" || + t === "symbol" + ) return a === b; return JSON.stringify(a) === JSON.stringify(b); } - function set(deviceData, path, timestamp, attributes, toClear) { path = deviceData.paths.add(path); @@ -269,20 +268,25 @@ function set(deviceData, path, timestamp, attributes, toClear) { let currentAttributes; - if (path.wildcard) - attributes = undefined; + if (path.wildcard) attributes = undefined; else if (currentTimestamp) currentAttributes = deviceData.attributes.get(path); let changeFlags = 0; if (attributes) { - if (attributes.value && attributes.value[1] - && attributes.value[0] >= (attributes.object ? attributes.object[0] : 0)) + if ( + attributes.value && + attributes.value[1] && + attributes.value[0] >= (attributes.object ? attributes.object[0] : 0) + ) attributes.object = [attributes.value[0], 0]; - if (attributes.object && attributes.object[1] - && attributes.object[0] >= (attributes.value ? attributes.value[0] : 0)) + if ( + attributes.object && + attributes.object[1] && + attributes.object[0] >= (attributes.value ? attributes.value[0] : 0) + ) attributes.value = [attributes.object[0]]; let newAttributes = Object.assign({}, currentAttributes, attributes); @@ -294,35 +298,45 @@ function set(deviceData, path, timestamp, attributes, toClear) { changeFlags |= CHANGE_FLAGS[attrName]; else if (attributes[attrName][0] <= currentAttributes[attrName][0]) newAttributes[attrName] = currentAttributes[attrName]; - else if (!compareEquality(attributes[attrName][1], currentAttributes[attrName][1])) + else if ( + !compareEquality( + attributes[attrName][1], + currentAttributes[attrName][1] + ) + ) changeFlags |= CHANGE_FLAGS[attrName]; } - } - else + } else { changeFlags |= 1; + } deviceData.attributes.set(path, newAttributes); if (!(timestamp <= currentTimestamp)) { deviceData.timestamps.set(path, timestamp); - if (path.length > 1) - toClear = set(deviceData, path.slice(0, path.length - 1), timestamp, - {object: [timestamp, 1]}, toClear); + if (path.length > 1) { + toClear = set( + deviceData, + path.slice(0, path.length - 1), + timestamp, + { object: [timestamp, 1] }, + toClear + ); + } } - } - else if (!(timestamp <= currentTimestamp)) { + } else if (!(timestamp <= currentTimestamp)) { deviceData.timestamps.set(path, timestamp); if (currentAttributes) { deviceData.attributes.delete(path); changeFlags |= 1; - } - else if (path.wildcard) { - for (let p of deviceData.paths.find(path, false, true, path.length)) + } else if (path.wildcard) { + for (let p of deviceData.paths.find(path, false, true, path.length)) { if (timestamp > deviceData.timestamps.get(p)) { toClear = toClear || []; toClear.push([p, timestamp]); } + } } } @@ -330,18 +344,14 @@ function set(deviceData, path, timestamp, attributes, toClear) { if (changeFlags & 1) { toClear = toClear || []; toClear.push([path, timestamp, null, changeFlags]); - } - else if (changeFlags & CHANGE_FLAGS.object) { + } else if (changeFlags & CHANGE_FLAGS.object) { toClear = toClear || []; - toClear.push([path, 0, {object: attributes.object[0]}, changeFlags]); - } - else { + toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]); + } else { for (let p of deviceData.paths.find(path, true, false, path.length)) { let tracker = deviceData.trackers.get(p); - for (let k in tracker) { - if (tracker[k] & changeFlags) - deviceData.changes.add(k); - } + for (let k in tracker) + if (tracker[k] & changeFlags) deviceData.changes.add(k); } } } @@ -349,14 +359,11 @@ function set(deviceData, path, timestamp, attributes, toClear) { return toClear; } - function track(deviceData, path, marker, attributes) { path = deviceData.paths.add(path); let f = 1; - if (attributes) - for (let attrName of attributes) - f |= CHANGE_FLAGS[attrName]; + if (attributes) for (let attrName of attributes) f |= CHANGE_FLAGS[attrName]; let cur = deviceData.trackers.get(path); if (!cur) { @@ -367,25 +374,17 @@ function track(deviceData, path, marker, attributes) { cur[marker] |= f; } - function clearTrackers(deviceData, tracker) { if (Array.isArray(tracker)) { - deviceData.trackers.forEach(function(v, k) { - for (let t of tracker) - delete v[t]; - }); - for (let t of tracker) - deviceData.changes.delete(t); - } - else { - deviceData.trackers.forEach(function(v, k) { - delete v[tracker]; - }); + for (let v of deviceData.trackers.values()) + for (let t of tracker) delete v[t]; + for (let t of tracker) deviceData.changes.delete(t); + } else { + for (let v of deviceData.trackers.values()) delete v[tracker]; deviceData.changes.delete(tracker); } } - exports.sanitizeParameterValue = sanitizeParameterValue; exports.getAliasDeclarations = getAliasDeclarations; exports.unpack = unpack; diff --git a/lib/extension-wrapper.js b/lib/extension-wrapper.js index f9d2512..d441bde 100644 --- a/lib/extension-wrapper.js +++ b/lib/extension-wrapper.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -23,11 +23,9 @@ const fileName = process.argv[2]; let script; function errorToFault(err) { - if (!err) - return null; + if (!err) return null; - if (!err.name) - return {code: "ext", message: `${err}`}; + if (!err.name) return { code: "ext", message: `${err}` }; let fault = { code: `ext.${err.name}`, @@ -41,7 +39,9 @@ function errorToFault(err) { if (err.stack) { fault.detail.stack = err.stack; // Trim the stack trace - let stackTrimIndex = fault.detail.stack.match(/\s+at\s[^\s]+\s\(.*extension-wrapper\.js:.+\)/); + let stackTrimIndex = fault.detail.stack.match( + /\s+at\s[^\s]+\s\(.*extension-wrapper\.js:.+\)/ + ); if (stackTrimIndex) fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index); } @@ -49,16 +49,16 @@ function errorToFault(err) { return fault; } -process.on("uncaughtException", function(err) { +process.on("uncaughtException", err => { let fault = errorToFault(err); - jobs.forEach(function(jobId) { + jobs.forEach(jobId => { process.send([jobId, fault]); }); jobs.clear(); process.disconnect(); }); -process.on("message", function(message) { +process.on("message", message => { jobs.add(message[0]); if (!script) { @@ -78,13 +78,12 @@ process.on("message", function(message) { return; } - script[funcName](message[1].slice(1), function(err, res) { - if (!jobs.delete(message[0])) - return; + script[funcName](message[1].slice(1), (err, res) => { + if (!jobs.delete(message[0])) return; process.send([message[0], errorToFault(err), res]); }); }); // Ignore SIGINT -process.on('SIGINT', function() {}); +process.on("SIGINT", () => {}); diff --git a/lib/extensions.js b/lib/extensions.js index bd891e8..a0dc997 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -19,11 +19,11 @@ "use strict"; const childProcess = require("child_process"); -const crypto = require ("crypto"); +const crypto = require("crypto"); const config = require("./config"); -const TIMEOUT = config.get('EXT_TIMEOUT'); +const TIMEOUT = config.get("EXT_TIMEOUT"); const processes = {}; const jobs = {}; @@ -36,82 +36,75 @@ function messageHandler(message) { } } - function run(args, callback) { let scriptName = args[0]; if (!processes[scriptName]) { - let p = childProcess.fork('lib/extension-wrapper', [scriptName]); + let p = childProcess.fork("lib/extension-wrapper", [scriptName]); processes[scriptName] = p; - p.on('error', function() { + p.on("error", () => { if (processes[scriptName] === p) { kill(processes[scriptName]); delete processes[scriptName]; } }); - p.on('disconnect', function() { + p.on("disconnect", () => { if (processes[scriptName] === p) { kill(processes[scriptName]); delete processes[scriptName]; } }); - p.on('message', messageHandler); + p.on("message", messageHandler); } - let id = crypto.randomBytes(8).toString('hex'); + let id = crypto.randomBytes(8).toString("hex"); jobs[id] = callback; - setTimeout(function() { + setTimeout(() => { if (id in jobs) { delete jobs[id]; - return callback(null, {code: "timeout", message: "Extension timed out"}); + return callback(null, { + code: "timeout", + message: "Extension timed out" + }); } }, TIMEOUT); return processes[scriptName].send([id, args.slice(1)]); -}; - +} function kill(process, callback) { - if (process.signalCode !== null || process.exitCode !== null) - return; + if (process.signalCode !== null || process.exitCode !== null) return; let timeToKill = Date.now() + 5000; process.kill(); - let t = setInterval(function() { + let t = setInterval(() => { if (process.signalCode !== null || process.exitCode !== null) { clearInterval(t); - if (callback) - callback(); - } - else if (Date.now() > timeToKill) { - process.kill('SIGKILL'); + if (callback) callback(); + } else if (Date.now() > timeToKill) { + process.kill("SIGKILL"); clearInterval(t); - if (callback) - callback(); + if (callback) callback(); } }, 100); } - function killAll(callback) { let counter = 1; for (let n in processes) { ++counter; - kill(processes[n], function() { - if (--counter == 0 && callback) - callback(); + kill(processes[n], () => { + if (--counter == 0 && callback) callback(); }); delete processes[n]; } - if (--counter == 0 && callback) - callback(); + if (--counter == 0 && callback) callback(); } - exports.run = run; exports.killAll = killAll; diff --git a/lib/fs.js b/lib/fs.js index c2062ef..90337d3 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -28,7 +28,7 @@ function listener(request, response) { if (request.method === "GET") { const filename = querystring.unescape(urlParts.pathname.substring(1)); const gs = new mongodb.GridStore(db.mongoDb, filename, "r", {}); - gs.open((err, gs) => { + gs.open(err => { if (err) { response.writeHead(404); response.end(); @@ -42,9 +42,7 @@ function listener(request, response) { stream.pipe(response); }); } else { - response.writeHead(405, { - "Allow": "GET" - }); + response.writeHead(405, { Allow: "GET" }); response.end("405 Method Not Allowed"); } } diff --git a/lib/gpn-heuristic.js b/lib/gpn-heuristic.js index d27ecb3..c91728e 100644 --- a/lib/gpn-heuristic.js +++ b/lib/gpn-heuristic.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -41,32 +41,31 @@ function estimateGpnCount(gpnPatterns, depth = 0) { let k = path[depth]; if (!k) { - if (flags & 1) - gpnCount = 1; + if (flags & 1) gpnCount = 1; continue; } if (flags & 1) { gpnCount = 1; - if (depth > UNDISCOVERED_DEPTH) - continue; - } - else if (k === "*") + if (depth > UNDISCOVERED_DEPTH) continue; + } else if (k === "*") { wildcardDiscovered = true; + } if (k === "*") { wildcardChildren.push(pattern); - } - else { + } else { children[k] = children[k] || []; children[k].push(pattern); } } let wildcardGpnCount = 0; - if (!wildcardDiscovered && wildcardChildren.length) - wildcardGpnCount += estimateGpnCount(wildcardChildren, depth + 1) * WILDCARD_MULTIPLIER; + if (!wildcardDiscovered && wildcardChildren.length) { + wildcardGpnCount += + estimateGpnCount(wildcardChildren, depth + 1) * WILDCARD_MULTIPLIER; + } for (let k of Object.keys(children)) { let c = estimateGpnCount(children[k].concat(wildcardChildren), depth + 1); diff --git a/lib/instance-set.js b/lib/instance-set.js index 20805b8..91641aa 100644 --- a/lib/instance-set.js +++ b/lib/instance-set.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -18,9 +18,7 @@ */ "use strict"; - class InstanceSet { - constructor() { this.set = new Set(); } @@ -44,29 +42,23 @@ class InstanceSet { } } - if (match) - res.push(inst); + if (match) res.push(inst); } - res.sort(function(a, b) { + res.sort((a, b) => { let keysA = Object.keys(a); let keysB = Object.keys(b); - if (keysA.length != keysB.length) - return keysB.length - keysA.length; + if (keysA.length != keysB.length) return keysB.length - keysA.length; keysA.sort(); keysB.sort(); - for (let i = 0; i < keysA.length; ++ i) { - if (keysA[i] > keysB[i]) - return 1; - else if (keysA[i] < keysB[i]) - return -1; - else if (a[keysA[i]] > b[keysB[i]]) - return 1; - else if (a[keysA[i]] < b[keysB[i]]) - return -1; + for (let i = 0; i < keysA.length; ++i) { + if (keysA[i] > keysB[i]) return 1; + else if (keysA[i] < keysB[i]) return -1; + else if (a[keysA[i]] > b[keysB[i]]) return 1; + else if (a[keysA[i]] < b[keysB[i]]) return -1; } return 0; @@ -87,29 +79,23 @@ class InstanceSet { } } - if (match) - res.push(inst); + if (match) res.push(inst); } - res.sort(function(a, b) { + res.sort((a, b) => { let keysA = Object.keys(a); let keysB = Object.keys(b); - if (keysA.length != keysB.length) - return keysA.length - keysB.length; + if (keysA.length != keysB.length) return keysA.length - keysB.length; keysA.sort(); keysB.sort(); - for (let i = 0; i < keysA.length; ++ i) { - if (keysA[i] > keysB[i]) - return 1; - else if (keysA[i] < keysB[i]) - return -1; - else if (a[keysA[i]] > b[keysB[i]]) - return 1; - else if (a[keysA[i]] < b[keysB[i]]) - return -1; + for (let i = 0; i < keysA.length; ++i) { + if (keysA[i] > keysB[i]) return 1; + else if (keysA[i] < keysB[i]) return -1; + else if (a[keysA[i]] > b[keysB[i]]) return 1; + else if (a[keysA[i]] < b[keysB[i]]) return -1; } return 0; diff --git a/lib/local-cache.js b/lib/local-cache.js index cd50c2e..a6618b8 100644 --- a/lib/local-cache.js +++ b/lib/local-cache.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -33,7 +33,6 @@ let nextRefresh = 1; let hash = null; let presets, provisions, virtualParameters, files; - function computeHash() { // MD5 hash for presets, provisions, virtual parameters for detecting changes let h = crypto.createHash("md5"); @@ -50,13 +49,11 @@ function computeHash() { keys = Object.keys(provisions).sort(); h.update(JSON.stringify(keys)); - for (let k of keys) - h.update(provisions[k].md5); + for (let k of keys) h.update(provisions[k].md5); keys = Object.keys(virtualParameters).sort(); h.update(JSON.stringify(keys)); - for (let k of keys) - h.update(virtualParameters[k].md5); + for (let k of keys) h.update(virtualParameters[k].md5); hash = h.digest("hex"); } @@ -66,47 +63,45 @@ function flattenObject(src, prefix = "", dst = {}) { let v = src[k]; if (typeof v === "object" && !Array.isArray(v)) flattenObject(v, `${prefix}${k}.`, dst); - else - dst[`${prefix}${k}`] = v; + else dst[`${prefix}${k}`] = v; } return dst; } function refresh(callback) { - if (!nextRefresh) - return setTimeout(function() {refresh(callback);}, 20); + if (!nextRefresh) { + return setTimeout(() => { + refresh(callback); + }, 20); + } nextRefresh = 0; const now = Date.now(); - cache.get("presets_hash", function(err, res) { - if (err) - return callback(err); + cache.get("presets_hash", (err, dbHash) => { + if (err) return callback(err); - if (hash && res === hash) { + if (hash && dbHash === hash) { nextRefresh = now + (REFRESH - (now % REFRESH)); return callback(); } - cache.lock("presets_hash_lock", 3, function(err, unlockOrExtend) { - if (err) - return callback(err); + cache.lock("presets_hash_lock", 3, (err, unlockOrExtend) => { + if (err) return callback(err); let counter = 3; counter += 2; - db.getPresets(function(err, res) { + db.getPresets((err, res) => { if (err) { - if (counter & 1) - callback(err); - return counter = 0; + if (counter & 1) callback(err); + return (counter = 0); } - db.getObjects(function(err, objects) { + db.getObjects((err, objects) => { if (err) { - if (counter & 1) - callback(err); - return counter = 0; + if (counter & 1) callback(err); + return (counter = 0); } objects = objects.map(obj => { @@ -118,14 +113,12 @@ function refresh(callback) { if (!obj["_keys"] || !obj["_keys"].length) obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_")); - return obj - }) + return obj; + }); res.sort((a, b) => { - if (a.weight === b.weight) - return a._id > b._id; - else - return a.weight - b.weight; + if (a.weight === b.weight) return a._id > b._id; + else return a.weight - b.weight; }); presets = []; @@ -134,14 +127,16 @@ function refresh(callback) { if (preset.schedule) { let parts = preset.schedule.trim().split(/\s+/); schedule = { - md5: crypto.createHash("md5").update(preset.schedule).digest("hex") + md5: crypto + .createHash("md5") + .update(preset.schedule) + .digest("hex") }; try { - schedule.duration = +(parts.shift()) * 1000; + schedule.duration = +parts.shift() * 1000; schedule.schedule = scheduling.parseCron(parts.join(" ")); - } - catch (err) { + } catch (err) { logger.warn({ message: "Invalid preset schedule", preset: preset._id, @@ -152,7 +147,9 @@ function refresh(callback) { } let events = preset.events || {}; - let precondition = query.convertMongoQueryToFilters(JSON.parse(preset.precondition)); + let precondition = query.convertMongoQueryToFilters( + JSON.parse(preset.precondition) + ); let _provisions = preset.provisions || []; // Generate provisions from the old configuration format @@ -181,32 +178,41 @@ function refresh(callback) { case "add_object": for (let obj of objects) { if (obj["_id"] === c.object) { - let alias = obj["_keys"].map((k) => `${k}:${JSON.stringify(obj[k])}`).join(","); + let alias = obj["_keys"] + .map(k => `${k}:${JSON.stringify(obj[k])}`) + .join(","); let p = `${c.name}.[${alias}]`; _provisions.push(["instances", p, 1]); - for (let k in obj) - if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1)) + for (let k in obj) { + if ( + !k.startsWith("_") && + !(obj["_keys"].indexOf(k) !== -1) + ) _provisions.push(["value", `${p}.${k}`, obj[k]]); + } } } + break; case "delete_object": for (let obj of objects) { if (obj["_id"] === c.object) { - let alias = obj["_keys"].map((k) => `${k}:${JSON.stringify(obj[k])}`).join(","); + let alias = obj["_keys"] + .map(k => `${k}:${JSON.stringify(obj[k])}`) + .join(","); let p = `${c.name}.[${alias}]`; _provisions.push(["instances", p, 0]); } } + break; default: if (counter & 1) callback(new Error(`Unknown configuration type ${c.type}`)); - return counter = 0; - break; + return (counter = 0); } } @@ -222,7 +228,7 @@ function refresh(callback) { if ((counter -= 2) === 1) { computeHash(); - cache.set("presets_hash", hash, 300, function(err) { + cache.set("presets_hash", hash, 300, err => { unlockOrExtend(0); nextRefresh = now + (REFRESH - (now % REFRESH)); callback(err); @@ -232,23 +238,28 @@ function refresh(callback) { }); counter += 2; - db.getProvisions(function(err, res) { + db.getProvisions((err, res) => { if (err) { - if (counter & 1) - callback(err); - return counter = 0; + if (counter & 1) callback(err); + return (counter = 0); } provisions = {}; for (let r of res) { provisions[r._id] = {}; - provisions[r._id].md5 = crypto.createHash("md5").update(r.script).digest("hex"); - provisions[r._id].script = new vm.Script(`"use strict";(function(){\n${r.script}\n})();`, {filename: r._id, lineOffset: -1, timeout: 50}); + provisions[r._id].md5 = crypto + .createHash("md5") + .update(r.script) + .digest("hex"); + provisions[r._id].script = new vm.Script( + `"use strict";(function(){\n${r.script}\n})();`, + { filename: r._id, lineOffset: -1, timeout: 50 } + ); } if ((counter -= 2) === 1) { computeHash(); - cache.set("presets_hash", hash, 300, function(err) { + cache.set("presets_hash", hash, 300, err => { unlockOrExtend(0); nextRefresh = now + (REFRESH - (now % REFRESH)); callback(err); @@ -257,23 +268,28 @@ function refresh(callback) { }); counter += 2; - db.getVirtualParameters(function(err, res) { + db.getVirtualParameters((err, res) => { if (err) { - if (counter & 1) - callback(err); - return counter = 0; + if (counter & 1) callback(err); + return (counter = 0); } virtualParameters = {}; for (let r of res) { - virtualParameters[r._id] = {} - virtualParameters[r._id].md5 = crypto.createHash("md5").update(r.script).digest("hex"); - virtualParameters[r._id].script = new vm.Script(`"use strict";(function(){\n${r.script}\n})();`, {filename: r._id, lineOffset: -1, timeout: 50}); + virtualParameters[r._id] = {}; + virtualParameters[r._id].md5 = crypto + .createHash("md5") + .update(r.script) + .digest("hex"); + virtualParameters[r._id].script = new vm.Script( + `"use strict";(function(){\n${r.script}\n})();`, + { filename: r._id, lineOffset: -1, timeout: 50 } + ); } if ((counter -= 2) === 1) { computeHash(); - cache.set("presets_hash", hash, 300, function(err) { + cache.set("presets_hash", hash, 300, err => { unlockOrExtend(0); nextRefresh = now + (REFRESH - (now % REFRESH)); callback(err); @@ -282,11 +298,10 @@ function refresh(callback) { }); counter += 2; - db.getFiles(function(err, res) { + db.getFiles((err, res) => { if (err) { - if (counter & 1) - callback(err); - return counter = 0; + if (counter & 1) callback(err); + return (counter = 0); } files = {}; @@ -300,7 +315,7 @@ function refresh(callback) { if ((counter -= 2) === 1) { computeHash(); - cache.set("presets_hash", hash, 300, function(err) { + cache.set("presets_hash", hash, 300, err => { unlockOrExtend(0); nextRefresh = now + (REFRESH - (now % REFRESH)); callback(err); @@ -310,7 +325,7 @@ function refresh(callback) { if ((counter -= 2) === 1) { computeHash(); - cache.set("presets_hash", hash, 300, function(err) { + cache.set("presets_hash", hash, 300, err => { unlockOrExtend(0); nextRefresh = now + (REFRESH - (now % REFRESH)); callback(err); @@ -320,37 +335,31 @@ function refresh(callback) { }); } - function getPresets(callback) { - if (Date.now() < nextRefresh) - return callback(null, hash, presets); + if (Date.now() < nextRefresh) return callback(null, hash, presets); - refresh(function(err) { + refresh(err => { callback(err, hash, presets); }); } - function getFiles(callback) { - if (Date.now() < nextRefresh) - return callback(null, hash, files); + if (Date.now() < nextRefresh) return callback(null, hash, files); - refresh(function(err) { + refresh(err => { callback(err, hash, files); - }) + }); } - function getProvisionsAndVirtualParameters(callback) { if (Date.now() < nextRefresh) return callback(null, hash, provisions, virtualParameters); - refresh(function(err) { + refresh(err => { callback(err, hash, provisions, virtualParameters); }); } - exports.getPresets = getPresets; exports.getFiles = getFiles; exports.getProvisionsAndVirtualParameters = getProvisionsAndVirtualParameters; diff --git a/lib/logger.js b/lib/logger.js index b919071..63660f5 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -35,57 +35,61 @@ let ACCESS_LOG_SYSTEMD = false; let LOG_FILE, ACCESS_LOG_FILE; -let logStream = fs.createWriteStream(null, {fd: process.stderr.fd}); +let logStream = fs.createWriteStream(null, { fd: process.stderr.fd }); let logStat = fs.fstatSync(logStream.fd); -let accessLogStream = fs.createWriteStream(null, {fd: process.stdout.fd}); +let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd }); let accessLogStat = fs.fstatSync(accessLogStream.fd); - // Reopen if original files have been moved (e.g. logrotate) function reopen() { let counter = 1; if (LOG_FILE) { - ++ counter; - fs.stat(LOG_FILE, function(err, stat) { - if (err && !err.message.startsWith("ENOENT:")) - throw err; + ++counter; + fs.stat(LOG_FILE, (err, stat) => { + if (err && !err.message.startsWith("ENOENT:")) throw err; if (!(stat && stat.dev === logStat.dev && stat.ino === logStat.ino)) { logStream.end(); - logStream = fs.createWriteStream(null, - {fd: fs.openSync(LOG_FILE, "a")}); + logStream = fs.createWriteStream(null, { + fd: fs.openSync(LOG_FILE, "a") + }); logStat = fs.fstatSync(logStream.fd); } - if (-- counter === 0) + if (--counter === 0) setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); }); } if (ACCESS_LOG_FILE) { - ++ counter; - fs.stat(ACCESS_LOG_FILE, function(err, stat) { - if (err && !err.message.startsWith("ENOENT:")) - throw err; + ++counter; + fs.stat(ACCESS_LOG_FILE, (err, stat) => { + if (err && !err.message.startsWith("ENOENT:")) throw err; - if (!(stat && stat.dev === accessLogStat.dev && stat.ino === accessLogStat.ino)) { + if ( + !( + stat && + stat.dev === accessLogStat.dev && + stat.ino === accessLogStat.ino + ) + ) { accessLogStream.end(); - accessLogStream = fs.createWriteStream(null, - {fd: fs.openSync(ACCESS_LOG_FILE, "a")}); + accessLogStream = fs.createWriteStream(null, { + fd: fs.openSync(ACCESS_LOG_FILE, "a") + }); accessLogStat = fs.fstatSync(accessLogStream.fd); } - if (-- counter === 0) + if (--counter === 0) setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); }); } - if (-- counter === 0) + if (--counter === 0) setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); } - function init(service, version) { defaultMeta.hostname = os.hostname(); defaultMeta.pid = process.pid; @@ -96,14 +100,14 @@ function init(service, version) { ACCESS_LOG_FILE = config.get(`${service.toUpperCase()}_ACCESS_LOG_FILE`); if (LOG_FILE) { - logStream = fs.createWriteStream(null, - {fd: fs.openSync(LOG_FILE, "a")}); + logStream = fs.createWriteStream(null, { fd: fs.openSync(LOG_FILE, "a") }); logStat = fs.fstatSync(logStream.fd); } if (ACCESS_LOG_FILE) { - accessLogStream = fs.createWriteStream(null, - {fd: fs.openSync(ACCESS_LOG_FILE, "a")}); + accessLogStream = fs.createWriteStream(null, { + fd: fs.openSync(ACCESS_LOG_FILE, "a") + }); accessLogStat = fs.fstatSync(accessLogStream.fd); } @@ -115,27 +119,26 @@ function init(service, version) { dev = parseInt(dev); inode = parseInt(inode); - LOG_SYSTEMD = (logStat.dev === dev && logStat.ino === inode); - ACCESS_LOG_SYSTEMD = (accessLogStat.dev === dev && accessLogStat.ino === inode); + LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode; + ACCESS_LOG_SYSTEMD = + accessLogStat.dev === dev && accessLogStat.ino === inode; } - if (LOG_FILE || ACCESS_LOG_FILE) { + if (LOG_FILE || ACCESS_LOG_FILE) // Can't use setInterval as we need all workers to cehck at the same time setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); - } } - function close() { accessLogStream.end(); logStream.end(); } - function flatten(details) { if (details.sessionContext) { details.deviceId = details.sessionContext.deviceId; - details.remoteAddress = details.sessionContext.httpRequest.connection.remoteAddress; + details.remoteAddress = + details.sessionContext.httpRequest.connection.remoteAddress; delete details.sessionContext; } @@ -157,20 +160,17 @@ function flatten(details) { details.acsRequestName = details.rpc.acsRequest.name; if (details.rpc.acsRequest.commandKey) details.acsRequestCommandKey = details.rpc.acsRequest.commandKey; - } - else if (details.rpc.cpeRequest) { + } else if (details.rpc.cpeRequest) { details.cpeRequestId = details.rpc.id; if (details.rpc.cpeRequest.name === "Inform") { details.informEvent = details.rpc.cpeRequest.event.join(","); details.informRetryCount = details.rpc.cpeRequest.retryCount; - } - else { + } else { details.cpeRequestName = details.rpc.cpeRequest.name; if (details.rpc.cpeRequest.commandKey) details.cpeRequestCommandKey = details.rpc.cpeRequest.commandKey; } - } - else if (details.rpc.cpeFault) { + } else if (details.rpc.cpeFault) { details.acsRequestId = details.rpc.id; details.cpeFaultCode = details.rpc.cpeFault.detail.faultCode; details.cpeFaultString = details.rpc.cpeFault.detail.faultString; @@ -187,16 +187,12 @@ function flatten(details) { return details; } - function formatJson(details, systemd) { if (systemd) { let severity = ""; - if (details.severity === "info") - severity = "<6>"; - else if (details.severity === "warn") - severity = "<4>"; - else if (details.severity === "error") - severity = "<3>"; + if (details.severity === "info") severity = "<6>"; + else if (details.severity === "warn") severity = "<4>"; + else if (details.severity === "error") severity = "<3>"; return `${severity}${JSON.stringify(flatten(details))}${os.EOL}`; } @@ -204,7 +200,6 @@ function formatJson(details, systemd) { return `${JSON.stringify(flatten(details))}${os.EOL}`; } - function formatSimple(details, systemd) { flatten(details); @@ -212,8 +207,7 @@ function formatSimple(details, systemd) { if (details.remoteAddress) { if (details.deviceId) remote = `${details.remoteAddress} ${details.deviceId}: `; - else - remote = `${details.remoteAddress}: `; + else remote = `${details.remoteAddress}: `; } const keys = Object.keys(details); @@ -229,28 +223,24 @@ function formatSimple(details, systemd) { let kv = []; for (let k of keys) - if (!skip[k]) - kv.push(`${k}=${JSON.stringify(details[k])}`); + if (!skip[k]) kv.push(`${k}=${JSON.stringify(details[k])}`); - if (kv.length) - meta = `; ${kv.join(" ")}`; + if (kv.length) meta = `; ${kv.join(" ")}`; if (systemd) { let severity = ""; - if (details.severity === "info") - severity = "<6>"; - else if (details.severity === "warn") - severity = "<4>"; - else if (details.severity === "error") - severity = "<3>"; + if (details.severity === "info") severity = "<6>"; + else if (details.severity === "warn") severity = "<4>"; + else if (details.severity === "error") severity = "<3>"; return `${severity}${remote}${details.message}${meta}${os.EOL}`; } - return `${details.timestamp} [${details.severity.toUpperCase()}] ${remote}${details.message}${meta}${os.EOL}`; + return `${details.timestamp} [${details.severity.toUpperCase()}] ${remote}${ + details.message + }${meta}${os.EOL}`; } - function log(details) { details.timestamp = new Date().toISOString(); if (LOG_FORMAT === "json") { @@ -261,25 +251,21 @@ function log(details) { } } - function info(details) { details.severity = "info"; log(details); } - function warn(details) { details.severity = "warn"; log(details); } - function error(details) { details.severity = "error"; log(details); } - function accessLog(details) { details.timestamp = new Date().toISOString(); if (ACCESS_LOG_FORMAT === "json") { @@ -290,25 +276,21 @@ function accessLog(details) { } } - function accessInfo(details) { details.severity = "info"; accessLog(details); } - function accessWarn(details) { details.severity = "warn"; accessLog(details); } - function accessError(details) { details.severity = "error"; accessLog(details); } - exports.init = init; exports.close = close; exports.flatten = flatten; diff --git a/lib/nbi.js b/lib/nbi.js index 52af579..2ec7e83 100644 --- a/lib/nbi.js +++ b/lib/nbi.js @@ -31,22 +31,22 @@ 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 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\-\_\%\:]+)\/?$/; +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.writeHead(500, { Connection: "close" }); httpResponse.end(`${err.name}: ${err.message}`); } throw err; @@ -57,7 +57,7 @@ function listener(request, response) { let bytes = 0; response.setHeader("GenieACS-Version", VERSION); - request.addListener("data", (chunk) => { + request.addListener("data", chunk => { chunks.push(chunk); bytes += chunk.length; }); @@ -77,75 +77,73 @@ function listener(request, response) { 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]); + 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); + db.presetsCollection.save(preset, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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); + db.presetsCollection.remove({ _id: presetName }, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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.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]); + 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); + db.objectsCollection.save(object, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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); + db.objectsCollection.remove({ _id: objectName }, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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.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]); + const provisionName = querystring.unescape( + PROVISIONS_REGEX.exec(urlParts.pathname)[1] + ); if (request.method === "PUT") { const object = { _id: provisionName, @@ -160,37 +158,35 @@ function listener(request, response) { return; } - db.provisionsCollection.save(object, (err) => { - if (err) - return throwError(err, response); + db.provisionsCollection.save(object, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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); + db.provisionsCollection.remove({ _id: provisionName }, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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.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]); + const virtualParameterName = querystring.unescape( + VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1] + ); if (request.method === "PUT") { const object = { _id: virtualParameterName, @@ -205,33 +201,32 @@ function listener(request, response) { return; } - db.virtualParametersCollection.save(object, (err) => { - if (err) - return throwError(err, response); + db.virtualParametersCollection.save(object, err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (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); + db.virtualParametersCollection.remove( + { _id: virtualParameterName }, + err => { + if (err) return throwError(err, response); - cache.del("presets_hash", (err) => { - if (err) - return throwError(err, response); + cache.del("presets_hash", err => { + if (err) return throwError(err, response); - response.writeHead(200); - response.end(); - }); - }); + response.writeHead(200); + response.end(); + }); + } + ); } else { - response.writeHead(405, {"Allow": "PUT, DELETE"}); + response.writeHead(405, { Allow: "PUT, DELETE" }); response.end("405 Method Not Allowed"); } } else if (TAGS_REGEX.test(urlParts.pathname)) { @@ -240,51 +235,48 @@ function listener(request, response) { 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); + { _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); + { _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.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 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); + 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); + 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); + cache.del(`${deviceId}_tasks_faults_operations`, err => { + if (err) return throwError(err, response); response.writeHead(200); response.end(); @@ -292,67 +284,81 @@ function listener(request, response) { }); } - cache.del(`${deviceId}_tasks_faults_operations`, (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": "DELETE"}); + 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]); + 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); + apiFunctions.insertTasks(task, err => { + if (err) return throwError(err, response); - cache.del(`${deviceId}_tasks_faults_operations`, (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) => { + apiFunctions.connectionRequest(deviceId, err => { if (err) { - response.writeHead(202, err.message, {"Content-Type": "application/json"}); + 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); + 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); + 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, task2) => { + if (err) return throwError(err, response); - response.writeHead(202, "Task faulted", { + response.writeHead(202, "Task faulted", { + "Content-Type": "application/json" + }); + response.end(JSON.stringify(task2)); + } + ); + } else { + response.writeHead(200, { "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, { @@ -364,7 +370,7 @@ function listener(request, response) { }); } else if (urlParts.query.connection_request != null) { // No task, send connection request only - apiFunctions.connectionRequest(deviceId, (err) => { + apiFunctions.connectionRequest(deviceId, err => { if (err) { response.writeHead(504); response.end(`${err.name}: ${err.message}`); @@ -378,7 +384,7 @@ function listener(request, response) { response.end(); } } else { - response.writeHead(405, {"Allow": "POST"}); + response.writeHead(405, { Allow: "POST" }); response.end("405 Method Not Allowed"); } } else if (TASKS_REGEX.test(urlParts.pathname)) { @@ -388,63 +394,70 @@ function listener(request, response) { 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); + { _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; + 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(); + }); + } + ); + } + ); } - - 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.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); + 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); + 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); + cache.del(`${deviceId}_tasks_faults_operations`, err => { + if (err) return throwError(err, response); - response.writeHead(200); - response.end(); - }); - }); - }); + response.writeHead(200); + response.end(); + }); + } + ); + } + ); } else { - response.writeHead(405, {"Allow": "POST"}); + response.writeHead(405, { Allow: "POST" }); response.end("405 Method Not Allowed"); } } else { @@ -452,7 +465,9 @@ function listener(request, response) { response.end(); } } else if (FILES_REGEX.test(urlParts.pathname)) { - const filename = querystring.unescape(FILES_REGEX.exec(urlParts.pathname)[1]); + const filename = querystring.unescape( + FILES_REGEX.exec(urlParts.pathname)[1] + ); if (request.method === "PUT") { const metadata = { fileType: request.headers.filetype, @@ -463,17 +478,14 @@ function listener(request, response) { const gs = new mongodb.GridStore(db.mongoDb, filename, filename, "w", { metadata: metadata }); - gs.open((err, gs) => { - if (err) - return throwError(err, response); + gs.open(err => { + if (err) return throwError(err, response); - gs.write(body, (err) => { - 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); + gs.close(err => { + if (err) return throwError(err, response); response.writeHead(201); response.end(); @@ -481,22 +493,21 @@ function listener(request, response) { }); }); } else if (request.method === "DELETE") { - mongodb.GridStore.unlink(db.mongoDb, filename, (err) => { - if (err) - return throwError(err, response); + 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.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.writeHead(404, { "Cache-Control": "no-cache" }); response.end(`${err.name}: ${err.message}`); return; } @@ -509,15 +520,16 @@ function listener(request, response) { }); } else if (DELETE_DEVICE_REGEX.test(urlParts.pathname)) { if (request.method !== "DELETE") { - response.writeHead(405, {"Allow": "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); + 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(); @@ -528,13 +540,15 @@ function listener(request, response) { // 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); + 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.writeHead(405, { Allow: "GET, HEAD" }); response.end("405 Method Not Allowed"); return; } @@ -563,14 +577,14 @@ function listener(request, response) { break; case "tasks": q = query.sanitizeQueryTypes(q, { - _id: (v) => new mongodb.ObjectID(v), - timestamp: (v) => new Date(v), + _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), + timestamp: v => new Date(v), retries: Number }); } @@ -582,7 +596,7 @@ function listener(request, response) { projection[p.trim()] = 1; } - const cur = collection.find(q, projection, {batchSize: 50}); + const cur = collection.find(q, projection, { batchSize: 50 }); if (urlParts.query.sort) { const s = JSON.parse(urlParts.query.sort); @@ -590,27 +604,24 @@ function listener(request, response) { for (let [k, v] of Object.entries(s)) { if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices") sort[`${k}._value`] = v; - else - sort[k] = v; + else sort[k] = v; } + cur.sort(sort); } - if (urlParts.query.skip) - cur.skip(parseInt(urlParts.query.skip)); + if (urlParts.query.skip) cur.skip(parseInt(urlParts.query.skip)); let limit; if (urlParts.query.limit) - cur.limit(limit = parseInt(urlParts.query.limit)); + cur.limit((limit = parseInt(urlParts.query.limit))); cur.count(false, (err, total) => { - if (err) - return throwError(err); - + if (err) return throwError(err); response.writeHead(200, { "Content-Type": "application/json", - "total": total + total: total }); if (request.method === "HEAD") { @@ -627,8 +638,7 @@ function listener(request, response) { } if (item) { - if (i++) - response.write(",\n"); + if (i++) response.write(",\n"); response.write(JSON.stringify(item)); } diff --git a/lib/path-set.js b/lib/path-set.js index 0f1333f..c00b15d 100644 --- a/lib/path-set.js +++ b/lib/path-set.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -18,25 +18,20 @@ */ "use strict"; - class PathSet { - constructor() { this.lengthIndex = []; this.fragmentIndex = []; } - get depth() { return this.lengthIndex.length; } - add(path) { let p = this.get(path); - if (p) - return p; + if (p) return p; if (!this.lengthIndex[path.length]) this.lengthIndex[path.length] = new Set(); @@ -44,13 +39,11 @@ class PathSet { this.lengthIndex[path.length].add(path); let wildcard = 0; - for (let i = 0; i < path.length; ++ i) { + for (let i = 0; i < path.length; ++i) { let fragment = path[i]; - if (fragment === "*") - wildcard = wildcard | (1 << i); + if (fragment === "*") wildcard = wildcard | (1 << i); - if (!this.fragmentIndex[i]) - this.fragmentIndex[i] = {}; + if (!this.fragmentIndex[i]) this.fragmentIndex[i] = {}; if (!this.fragmentIndex[i][fragment]) this.fragmentIndex[i][fragment] = new Set(); @@ -66,31 +59,26 @@ class PathSet { return path; } - get(pattern) { - if (!this.lengthIndex[pattern.length]) - return null; + if (!this.lengthIndex[pattern.length]) return null; - if (this.lengthIndex[pattern.length].has(pattern)) - return pattern; + if (this.lengthIndex[pattern.length].has(pattern)) return pattern; let sets = []; sets.push(this.lengthIndex[pattern.length]); - for (let i = 0; i < pattern.length; ++ i) { - if (!this.fragmentIndex[i]) - return null; + for (let i = 0; i < pattern.length; ++i) { + if (!this.fragmentIndex[i]) return null; let fragment = pattern[i]; - if (!this.fragmentIndex[i][fragment]) - return null; + if (!this.fragmentIndex[i][fragment]) return null; sets.push(this.fragmentIndex[i][fragment]); } - sets.sort(function(a, b) { + sets.sort((a, b) => { return a.size - b.size; }); @@ -99,36 +87,31 @@ class PathSet { for (let s of sets) { if (!s.has(path)) { path = null; - break + break; } } - if (path) - return path; + + if (path) return path; } return null; } - find(pattern, superset = false, subset = false, depth = pattern.length) { let res = []; let groups = [[]]; - for (let i = pattern.length; i <= depth; ++ i) - if (this.lengthIndex[i]) - groups[0].push(this.lengthIndex[i]); + for (let i = pattern.length; i <= depth; ++i) + if (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]); - if (!groups[0].length) - return res; + if (!groups[0].length) return res; - for (let i = 0; i < pattern.length; ++ i) { - if (!this.fragmentIndex[i]) - return res; + for (let i = 0; i < pattern.length; ++i) { + if (!this.fragmentIndex[i]) return res; let fragment = pattern[i]; - if (fragment === "*" && subset) - continue; + if (fragment === "*" && subset) continue; let g = []; @@ -138,15 +121,20 @@ class PathSet { if (fragment !== "*" && superset && this.fragmentIndex[i]["*"]) g.push(this.fragmentIndex[i]["*"]); - if (!g.length) - return res; + if (!g.length) return res; groups.push(g); } - groups.sort(function(a, b) { - return a.reduce(function(prev, cur) {return prev + cur.size;}, 0) - - b.reduce(function(prev, cur) {return prev + cur.size;}, 0); + groups.sort((a, b) => { + return ( + a.reduce((prev, cur) => { + return prev + cur.size; + }, 0) - + b.reduce((prev, cur) => { + return prev + cur.size; + }, 0) + ); }); let smallestGroup = groups.shift(); @@ -160,20 +148,18 @@ class PathSet { break; } } + if (!found) { path = null; break; } } - if (path) - res.push(path); + if (path) res.push(path); } } return res; } - } - module.exports = PathSet; diff --git a/lib/query.js b/lib/query.js index 8f82222..fe24064 100644 --- a/lib/query.js +++ b/lib/query.js @@ -21,21 +21,16 @@ const common = require("./common"); function stringToRegexp(input, flags) { - if (input.indexOf("*") === -1) - return false; + if (input.indexOf("*") === -1) return false; - let output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&"); - if (output[0] === "*") - output = output.replace(/^\*+/g, ""); - else - output = "^" + output; + let output = input.replace(/[[\]\\^$.|?+()]/, "\\$&"); + if (output[0] === "*") output = output.replace(/^\*+/g, ""); + else output = "^" + output; - if (output[output.length - 1] === "*") - output = output.replace(/\*+$/g, ""); - else - output = output + "$"; + if (output[output.length - 1] === "*") output = output.replace(/\*+$/g, ""); + else output = output + "$"; - output = output.replace(/[\*]/, ".*"); + output = output.replace(/[*]/, ".*"); return new RegExp(output, flags); } @@ -43,19 +38,15 @@ function normalize(input) { if (common.typeOf(input) === common.STRING_TYPE) { const vals = [input]; let m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input); - if (m) - vals.push({"$regex": new RegExp(m[1], m[2])}); + if (m) vals.push({ $regex: new RegExp(m[1], m[2]) }); - if (+input === parseFloat(input)) - vals.push(+input); + if (+input === parseFloat(input)) vals.push(+input); let d = new Date(input); - if (input.length >= 8 && d.getFullYear() > 1983) - vals.push(d); + if (input.length >= 8 && d.getFullYear() > 1983) vals.push(d); let r = stringToRegexp(input); - if (r !== false) - vals.push({"$regex": r}); + if (r !== false) vals.push({ $regex: r }); return vals; } @@ -64,16 +55,13 @@ function normalize(input) { function expandValue(value) { if (common.typeOf(value) === common.ARRAY_TYPE) { - const a = []; - for (let j of value) - a = a.conact(expandValue(j)); + let a = []; + for (let j of value) a = a.conact(expandValue(j)); return [a]; } else if (common.typeOf(value) !== common.OBJECT_TYPE) { const n = normalize(value); - if (common.typeOf(n) !== common.ARRAY_TYPE) - return [n]; - else - return n; + if (common.typeOf(n) !== common.ARRAY_TYPE) return [n]; + else return n; } const objs = []; @@ -89,14 +77,12 @@ function expandValue(value) { let i = 0; while (i < indices.length) { const obj = {}; - for (let j = 0; j < keys.length; ++ j) - obj[keys[j]] = values[j][indices[j]]; + for (let j = 0; j < keys.length; ++j) obj[keys[j]] = values[j][indices[j]]; objs.push(obj); - for (i = 0; i < indices.length; ++ i) { + for (i = 0; i < indices.length; ++i) { indices[i] += 1; - if (indices[i] < values[i].length) - break; + if (indices[i] < values[i].length) break; indices[i] = 0; } } @@ -107,11 +93,10 @@ function permute(param, val) { const conditions = []; const values = expandValue(val); - if (param[param.lastIndexOf(".") + 1] !== "_") - param += "._value"; + if (param[param.lastIndexOf(".") + 1] !== "_") param += "._value"; for (let v of values) { - const obj = {} + const obj = {}; obj[param] = v; conditions.push(obj); } @@ -122,7 +107,8 @@ function permute(param, val) { function expand(query) { const newQuery = {}; for (let [k, v] of Object.entries(query)) { - if (k[0] === "$") { // Operator + if (k[0] === "$") { + // Operator newQuery[k] = v.map(e => expand(e)); } else { const conditions = permute(k, v); @@ -131,16 +117,16 @@ function expand(query) { if (v && (v["$ne"] != null || v["$not"] != null)) { if (Object.keys(v).length > 1) throw new Error("Cannot mix $ne or $not with other operators"); - for (let c of conditions) - newQuery["$an"].push(c); + for (let c of conditions) newQuery["$an"].push(c); } else { - newQuery["$and"].push({"$or": conditions}); + newQuery["$and"].push({ $or: conditions }); } } else { Object.assign(newQuery, conditions[0]); } } } + return newQuery; } @@ -149,16 +135,13 @@ function testExpressions(params, expressions, lop) { const res = test(params, f); switch (lop) { case "$and": - if (!res) - return false; + if (!res) return false; break; case "$or": - if (res) - return true; + if (res) return true; break; case "$nor": - if (res) - return false; + if (res) return false; } } @@ -177,20 +160,18 @@ function testExpressions(params, expressions, lop) { function test(params, query) { let res; for (let [k, v] of Object.entries(query)) { - if (k.charAt(0) === "$") { // Logical operator + if (k.charAt(0) === "$") { + // Logical operator res = testExpressions(params, v, k); } else { const value = params[k]; if (common.typeOf(v) !== common.OBJECT_TYPE) { - // TODO comparing array to regex, array to array, and object to object if (common.typeOf(value) === common.ARRAY_TYPE) { + // TODO comparing array to regex, array to array, and object to object res = value.indexOf(v) !== -1; } else { - if (common.typeOf(v) === common.REGEXP_TYPE) { - res = v.test(value); - } else { - res = v === value; - } + if (common.typeOf(v) === common.REGEXP_TYPE) res = v.test(value); + else res = v === value; } } else { for (let [k2, v2] of Object.entries(v)) { @@ -198,8 +179,7 @@ function test(params, query) { case "$ne": if (common.typeOf(value) === common.ARRAY_TYPE) res = value.indexOf(v2) === -1; - else - res = value !== v2; + else res = value !== v2; break; case "$lt": res = value < v2; @@ -218,16 +198,12 @@ function test(params, query) { break; case "$in": throw new Error("Operator not supported"); - break; case "$nin": throw new Error("Operator not supported"); - break; case "$all": throw new Error("Operator not supported"); - break; case "$exists": throw new Error("Operator not supported"); - break; default: throw new Error("Operator not supported"); } @@ -235,8 +211,7 @@ function test(params, query) { } } - if (!res) - return false; + if (!res) return false; } return true; } @@ -246,49 +221,41 @@ function matchType(src, dst) { if (type === "string") { return String(dst); } else if (type === "number") { - if (+dst === parseFloat(dst)) - return +dst; - else if (!isNaN(Date.parse(dst))) - return Date.parse(dst); + if (+dst === parseFloat(dst)) return +dst; + else if (!isNaN(Date.parse(dst))) return Date.parse(dst); } else if (type === "boolean") { - const v = String(dst).trim().toLowerCase(); - if (v === "true" || v === "1") - return true; - else if (v === "false" || v === "0") - return false; + const v = String(dst) + .trim() + .toLowerCase(); + if (v === "true" || v === "1") return true; + else if (v === "false" || v === "0") return false; } return dst; } function testFilter(obj, filter) { for (let [k, v] of Object.entries(filter)) { - const [param, op] = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 2) + const [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2); const val = matchType(obj[param], v); switch (op) { case "=": case undefined: - if (obj[param] !== val) - return false; + if (obj[param] !== val) return false; break; case ">": - if (!(obj[param] > val)) - return false; + if (!(obj[param] > val)) return false; break; case ">=": - if (!(obj[param] >= val)) - return false; + if (!(obj[param] >= val)) return false; break; case "<": - if (!(obj[param] < val)) - return false; + if (!(obj[param] < val)) return false; break; case "<=": - if (!(obj[param] <= val)) - return false; + if (!(obj[param] <= val)) return false; break; case "!": - if (obj[param] === val) - return false; + if (obj[param] === val) return false; break; default: throw new Error(`Unrecognized operator ${op}`); @@ -298,25 +265,21 @@ function testFilter(obj, filter) { } function convertMongoQueryToFilters(query, filters) { - filters = filters || {} + filters = filters || {}; for (let [k, v] of Object.entries(query)) { if (k[0] === "$") { if (k === "$and") - for (let vv of v) - convertMongoQueryToFilters(vv, filters); - else - throw new Error(`Operator ${k} not supported`); + for (let vv of v) convertMongoQueryToFilters(vv, filters); + else throw new Error(`Operator ${k} not supported`); } else if (k === "_tags") { if (common.typeOf(v) === common.OBJECT_TYPE) { for (let [kk, vv] of Object.entries(v)) { - vv = vv.replace(/[^a-zA-Z0-9\-]+/g, "_"); - if (kk === "$ne") - filters[`Tags.${vv}!`] = true; - else - throw new Error(`Operator ${kk} not supported`); + vv = vv.replace(/[^a-zA-Z0-9-]+/g, "_"); + if (kk === "$ne") filters[`Tags.${vv}!`] = true; + else throw new Error(`Operator ${kk} not supported`); } } else { - v = v.replace(/[^a-zA-Z0-9\-]+/g, "_"); + v = v.replace(/[^a-zA-Z0-9-]+/g, "_"); filters[`Tags.${v}`] = true; } } else { @@ -379,22 +342,22 @@ function convertMongoQueryToFilters(query, filters) { } } } + return filters; } function sanitizeQueryTypes(query, types) { for (let [k, v] of Object.entries(query)) { - if (k[0] === "$") { // Logical operator - for (let vv of v) - sanitizeQueryTypes(vv, types); + if (k[0] === "$") { + // Logical operator + for (let vv of v) sanitizeQueryTypes(vv, types); } else if (k in types) { if (common.typeOf(v) === common.OBJECT_TYPE) { for (let [kk, vv] of Object.entries(v)) { switch (kk) { case "$in": case "$nin": - for (let i = 0; i < vv.length; ++ i) - vv[i] = types[k](vv[i]); + for (let i = 0; i < vv.length; ++i) vv[i] = types[k](vv[i]); break; case "$eq": case "$gt": @@ -417,6 +380,7 @@ function sanitizeQueryTypes(query, types) { } } } + return query; } diff --git a/lib/sandbox.js b/lib/sandbox.js index 8437e11..253986c 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -28,7 +28,6 @@ const extensions = require("./extensions"); const logger = require("./logger"); const scheduling = require("./scheduling"); - // Used for throwing to exit user script and commit const COMMIT = Symbol(); @@ -49,24 +48,20 @@ function runExtension(sessionContext, key, extCall, callback) { if (re[key]) { re[key].push(callback); - } - else { + } else { re[key] = [callback]; - extensions.run(extCall, function(err, fault, res) { + extensions.run(extCall, (err, fault, res) => { const callbacks = re[key]; delete re[key]; - if (!err && !fault) - sessionContext.extensionsCache[key] = res; - for (let c of callbacks) - c(err, fault); + if (!err && !fault) sessionContext.extensionsCache[key] = res; + for (let c of callbacks) c(err, fault); }); } } class SandboxDate { constructor() { - if (arguments.length) - return new Date(...arguments); + if (arguments.length) return new Date(...arguments); return new Date(state.sessionContext.timestamp); } @@ -75,120 +70,128 @@ class SandboxDate { let t = state.sessionContext.timestamp; if (typeof intervalOrCron === "number") { - - if (variance == null) - variance = intervalOrCron; + if (variance == null) variance = intervalOrCron; let offset = 0; if (variance) offset = scheduling.variance(state.sessionContext.deviceId, variance); t = scheduling.interval(t, intervalOrCron, offset); - } - else if (typeof intervalOrCron === "string") { + } else if (typeof intervalOrCron === "string") { let offset = 0; if (variance) offset = scheduling.variance(state.sessionContext.deviceId, variance); let cron = scheduling.parseCron(intervalOrCron); t = scheduling.cron(t, cron, offset)[0]; - } - else if (intervalOrCron) { + } else if (intervalOrCron) { throw new Error("Invalid Date.now() argument"); } return t; } -}; +} function random() { - if (!state.rng) - state.rng = seedrandom(state.sessionContext.deviceId); + if (!state.rng) state.rng = seedrandom(state.sessionContext.deviceId); return state.rng(); } random.seed = function(s) { state.rng = seedrandom(s); -} - +}; class ParameterWrapper { constructor(path, attributes, unpacked, unpackedRevision) { for (let attrName of attributes) { - Object.defineProperty(this, attrName, {get: function() { - if (state.uncommitted) - commit(); + Object.defineProperty(this, attrName, { + get: function() { + if (state.uncommitted) commit(); + + if (state.revision !== unpackedRevision) { + unpackedRevision = state.revision; + unpacked = device.unpack( + state.sessionContext.deviceData, + path, + state.revision + ); + } + + if (!unpacked.length) return undefined; + + let attr = state.sessionContext.deviceData.attributes.get( + unpacked[0], + state.revision + )[attrName]; + + if (!attr) return undefined; + + return attr[1]; + } + }); + } + + Object.defineProperty(this, "path", { + get: function() { + if (state.uncommitted) commit(); if (state.revision !== unpackedRevision) { unpackedRevision = state.revision; - unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision); + unpacked = device.unpack( + state.sessionContext.deviceData, + path, + state.revision + ); } - if (!unpacked.length) - return undefined; + if (!unpacked.length) return undefined; - let attr = state.sessionContext.deviceData.attributes.get(unpacked[0], state.revision)[attrName]; - - if (!attr) - return undefined; - - return attr[1]; - }}); - } - - Object.defineProperty(this, "path", {get: function() { - if (state.uncommitted) - commit(); - - if (state.revision !== unpackedRevision) { - unpackedRevision = state.revision; - unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision); + return unpacked[0].join("."); } + }); - if (!unpacked.length) - return undefined; + Object.defineProperty(this, "size", { + get: function() { + if (state.uncommitted) commit(); - return unpacked[0].join("."); - }}); + if (state.revision !== unpackedRevision) { + unpackedRevision = state.revision; + unpacked = device.unpack( + state.sessionContext.deviceData, + path, + state.revision + ); + } - Object.defineProperty(this, "size", {get: function() { - if (state.uncommitted) - commit(); + if (!unpacked.length) return undefined; - if (state.revision !== unpackedRevision) { - unpackedRevision = state.revision; - unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision); + return unpacked.length; } - - if (!unpacked.length) - return undefined; - - return unpacked.length; - }}); + }); this[Symbol.iterator] = function*() { - if (state.uncommitted) - commit(); + if (state.uncommitted) commit(); if (state.revision !== unpackedRevision) { unpackedRevision = state.revision; - unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision); + unpacked = device.unpack( + state.sessionContext.deviceData, + path, + state.revision + ); } for (let p of unpacked) yield new ParameterWrapper(p, attributes, [p], state.revision); - } + }; } } - function declare(path, timestamps, values) { state.uncommitted = true; - if (!timestamps) - timestamps = {}; + if (!timestamps) timestamps = {}; - if (!values) - values = {}; + if (!values) values = {}; let parsedPath = common.parsePath(path); @@ -196,25 +199,23 @@ function declare(path, timestamps, values) { let attrs = {}; for (let attrName in values) { - if (!timestamps[attrName]) - timestamps[attrName] = 1; + if (!timestamps[attrName]) timestamps[attrName] = 1; - if (attrName === "path") + if (attrName === "path") { declaration[3] = values.path; - else { + } else { attrs[attrName] = 1; declaration[4] = declaration[4] || {}; if (attrName === "value" && !Array.isArray(values.value)) declaration[4].value = [values.value]; - else - declaration[4][attrName] = values[attrName]; + else declaration[4][attrName] = values[attrName]; } } for (let attrName in timestamps) { - if (attrName === "path") + if (attrName === "path") { declaration[1] = timestamps.path; - else { + } else { attrs[attrName] = 1; declaration[2] = declaration[2] || {}; declaration[2][attrName] = timestamps[attrName]; @@ -226,7 +227,6 @@ function declare(path, timestamps, values) { return new ParameterWrapper(parsedPath, Object.keys(attrs)); } - function clear(path, timestamp, attributes) { state.uncommitted = true; @@ -234,20 +234,21 @@ function clear(path, timestamp, attributes) { state.clear.push([common.parsePath(path), timestamp, attributes]); } - function commit() { - ++ state.revision; + ++state.revision; state.uncommitted = false; - if (state.revision === state.maxRevision + 1) + if (state.revision === state.maxRevision + 1) { throw COMMIT; - else if (state.revision > state.maxRevision + 1) - throw new Error("Declare function should not be called from within a try/catch block"); + } else if (state.revision > state.maxRevision + 1) { + throw new Error( + "Declare function should not be called from within a try/catch block" + ); + } } - function ext() { - ++ state.extCounter; + ++state.extCounter; let extCall = Array.from(arguments).map(String); let key = `${state.revision}: ${JSON.stringify(extCall)}`; @@ -258,7 +259,6 @@ function ext() { throw EXT; } - function log(msg, meta) { if (state.revision === state.maxRevision && state.extCounter >= 0) { const details = Object.assign({}, meta, { @@ -277,26 +277,22 @@ function log(msg, meta) { } } - -Object.defineProperty(context, "Date", {value: SandboxDate}); -Object.defineProperty(context, "declare", {value: declare}); -Object.defineProperty(context, "clear", {value: clear}); -Object.defineProperty(context, "commit", {value: commit}); -Object.defineProperty(context, "ext", {value: ext}); -Object.defineProperty(context, "log", {value: log}); +Object.defineProperty(context, "Date", { value: SandboxDate }); +Object.defineProperty(context, "declare", { value: declare }); +Object.defineProperty(context, "clear", { value: clear }); +Object.defineProperty(context, "commit", { value: commit }); +Object.defineProperty(context, "ext", { value: ext }); +Object.defineProperty(context, "log", { value: log }); // Monkey-patch Math.random() to make it deterministic context.random = random; vm.runInContext("Math.random = random;", context); delete context.random; - function errorToFault(err) { - if (!err) - return null; + if (!err) return null; - if (!err.name) - return {code: "script", message: `${err}`}; + if (!err.name) return { code: "script", message: `${err}` }; let fault = { code: `script.${err.name}`, @@ -310,7 +306,9 @@ function errorToFault(err) { if (err.stack) { fault.detail.stack = err.stack; // Trim the stack trace at the self-executing anonymous wrapper function - let stackTrimIndex = fault.detail.stack.match(/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/); + let stackTrimIndex = fault.detail.stack.match( + /\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/ + ); if (stackTrimIndex) fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index); } @@ -318,8 +316,15 @@ function errorToFault(err) { return fault; } - -function run(script, globals, sessionContext, startRevision, maxRevision, callback, extCounter = 0) { +function run( + script, + globals, + sessionContext, + startRevision, + maxRevision, + callback, + extCounter = 0 +) { state = { sessionContext: sessionContext, revision: startRevision, @@ -332,24 +337,19 @@ function run(script, globals, sessionContext, startRevision, maxRevision, callba extCounter: extCounter }; - for (let n of Object.keys(context)) - delete context[n]; + for (let n of Object.keys(context)) delete context[n]; Object.assign(context, globals); let ret, status; try { - ret = script.runInContext(context, {displayErrors: false}); + ret = script.runInContext(context, { displayErrors: false }); status = 0; - } - catch (err) { - if (err === COMMIT) - status = 1; - else if (err === EXT) - status = 2; - else - return callback(null, errorToFault(err)); + } catch (err) { + if (err === COMMIT) status = 1; + else if (err === EXT) status = 2; + else return callback(null, errorToFault(err)); } let _state = state; @@ -363,26 +363,47 @@ function run(script, globals, sessionContext, startRevision, maxRevision, callba let counter = 3; for (let key of Object.keys(_state.extensions)) { counter += 2; - runExtension(_state.sessionContext, key, _state.extensions[key], function(err, fault) { - if (err || fault) { - if (counter & 1) - callback(err, fault); - return counter = 0; + runExtension( + _state.sessionContext, + key, + _state.extensions[key], + (err, fault) => { + if (err || fault) { + if (counter & 1) callback(err, fault); + return (counter = 0); + } + if ((counter -= 2) === 1) { + if (status === 2) { + return run.apply(null, args); + } else { + return callback( + null, + null, + _state.clear, + _state.declarations, + status === 0, + ret + ); + } + } } - if ((counter -= 2) === 1) - if (status === 2) - return run.apply(null, args); - else - return callback(null, null, _state.clear, _state.declarations, status === 0, ret); - }); + ); } - if ((counter -= 2) === 1) - if (status === 2) + if ((counter -= 2) === 1) { + if (status === 2) { return run.apply(null, args); - else - return callback(null, null, _state.clear, _state.declarations, status === 0, ret); + } else { + return callback( + null, + null, + _state.clear, + _state.declarations, + status === 0, + ret + ); + } + } } - exports.run = run; diff --git a/lib/scheduling.js b/lib/scheduling.js index 2a4c8aa..d30b586 100644 --- a/lib/scheduling.js +++ b/lib/scheduling.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -22,7 +22,6 @@ const crypto = require("crypto"); const later = require("later"); - function md532(str) { const digest = crypto .createHash("md5") @@ -36,42 +35,34 @@ function md532(str) { ); } - -function variance(deviceId, variance) { - return (md532(deviceId) >>> 0) % variance; +function variance(deviceId, vrnc) { + return (md532(deviceId) >>> 0) % vrnc; } - -function interval(timestamp, interval, offset = 0) { - return Math.trunc((timestamp + offset) / interval) * interval - offset; +function interval(timestamp, intrvl, offset = 0) { + return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset; } - function parseCron(cronExp) { let parts = cronExp.trim().split(/\s+/); - if (parts.length === 5) - parts.unshift("*"); + if (parts.length === 5) parts.unshift("*"); return later.schedule(later.parse.cron(parts.join(" "), true)); } - function cron(timestamp, schedule, offset = 0) { // TODO later.js doesn't throw erorr if expression is invalid! const ret = [0, 0]; let prev = schedule.prev(1, new Date(timestamp + offset)); - if (prev) - ret[0] = prev.setMilliseconds(0) - offset; + if (prev) ret[0] = prev.setMilliseconds(0) - offset; let next = schedule.next(1, new Date(timestamp + offset + 1000)); - if (next) - ret[1] = next.setMilliseconds(0) - offset; + if (next) ret[1] = next.setMilliseconds(0) - offset; return ret; } - exports.variance = variance; exports.interval = interval; exports.parseCron = parseCron; diff --git a/lib/server.js b/lib/server.js index 047f97c..f94bc66 100644 --- a/lib/server.js +++ b/lib/server.js @@ -20,8 +20,7 @@ const service = process.argv[2]; -if (service == null) - throw new Error("Missing argument cwmp, fs, or nbi"); +if (service == null) throw new Error("Missing argument cwmp, fs, or nbi"); const cluster = require("cluster"); const path = require("path"); @@ -39,17 +38,15 @@ const networkInterface = config.get(`${service.toUpperCase()}_INTERFACE`); const port = config.get(`${service.toUpperCase()}_PORT`); const useHttps = config.get(`${service.toUpperCase()}_SSL`); -const {listener, onConnection} = require(`./${service}`); +const { listener, onConnection } = require(`./${service}`); let server; function closeServer(timeout, callback) { - if (!server) - return callback(); + if (!server) return callback(); setTimeout(() => { - if (!callback) - return; + if (!callback) return; // Ignore HTTP requests from connection that may still be open server.removeListener("request", listener); @@ -60,9 +57,8 @@ function closeServer(timeout, callback) { setTimeout(cb, 1000); }, timeout).unref(); - server.close(function() { - if (!callback) - return; + server.close(() => { + if (!callback) return; let cb = callback; callback = null; @@ -81,12 +77,11 @@ function exit() { db.disconnect(); cache.disconnect(); extensions.killAll(); - if (cluster.worker) - cluster.worker.disconnect(); + if (cluster.worker) cluster.worker.disconnect(); }); } -process.on("uncaughtException", (err) => { +process.on("uncaughtException", err => { logger.error({ message: "Uncaught exception", exception: err, @@ -98,7 +93,10 @@ process.on("uncaughtException", (err) => { if (useHttps) { const httpsKey = path.resolve(config.get("CONFIG_DIR"), `${service}.key`); const httpsCert = path.resolve(config.get("CONFIG_DIR"), `${service}.crt`); - const httpsCa = path.resolve(config.get("CONFIG_DIR"), `${service}.ca-bundle`); + const httpsCa = path.resolve( + config.get("CONFIG_DIR"), + `${service}.ca-bundle` + ); const options = { key: fs.readFileSync(httpsKey), cert: fs.readFileSync(httpsCert) @@ -106,24 +104,25 @@ if (useHttps) { try { // Use intermediate certificates if available - options.ca = fs.readFileSync(httpsCa).toString().match(/\-+BEGIN CERTIFICATE\-+[0-9a-zA-Z\+\-\/\=\s]+?\-+END CERTIFICATE\-+/g); - } catch (error) {} + options.ca = fs + .readFileSync(httpsCa) + .toString() + .match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g); + } catch (error) { + // No intermediate certificate + } server = https.createServer(options, listener); - if (onConnection != null) - server.on("secureConnection", onConnection); + if (onConnection != null) server.on("secureConnection", onConnection); } else { server = http.createServer(listener); - if (onConnection != null) - server.on("connection", onConnection); + if (onConnection != null) server.on("connection", onConnection); } -db.connect((err) => { - if (err) - throw err; +db.connect(err => { + if (err) throw err; - cache.connect(function(err) { - if (err) - throw err; + cache.connect(err => { + if (err) throw err; server.listen(port, networkInterface); }); diff --git a/lib/soap.js b/lib/soap.js index 481b781..46a2c75 100644 --- a/lib/soap.js +++ b/lib/soap.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -28,50 +28,50 @@ const NAMESPACES = { "1.0": { "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/", - "xsd": "http://www.w3.org/2001/XMLSchema", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "cwmp": "urn:dslforum-org:cwmp-1-0" + xsd: "http://www.w3.org/2001/XMLSchema", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + cwmp: "urn:dslforum-org:cwmp-1-0" }, "1.1": { "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/", - "xsd": "http://www.w3.org/2001/XMLSchema", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "cwmp": "urn:dslforum-org:cwmp-1-1" + xsd: "http://www.w3.org/2001/XMLSchema", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + cwmp: "urn:dslforum-org:cwmp-1-1" }, "1.2": { "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/", - "xsd": "http://www.w3.org/2001/XMLSchema", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "cwmp": "urn:dslforum-org:cwmp-1-2" + xsd: "http://www.w3.org/2001/XMLSchema", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + cwmp: "urn:dslforum-org:cwmp-1-2" }, "1.3": { "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/", - "xsd": "http://www.w3.org/2001/XMLSchema", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "cwmp": "urn:dslforum-org:cwmp-1-2" + xsd: "http://www.w3.org/2001/XMLSchema", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + cwmp: "urn:dslforum-org:cwmp-1-2" }, "1.4": { "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/", - "xsd": "http://www.w3.org/2001/XMLSchema", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", - "cwmp": "urn:dslforum-org:cwmp-1-3" + xsd: "http://www.w3.org/2001/XMLSchema", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + cwmp: "urn:dslforum-org:cwmp-1-3" } }; const LIBXMLJS_PARSE_OPTIONS = { - "nocdata": true, - "recover": config.get("XML_RECOVER"), - "ignore_enc": config.get("XML_IGNORE_ENC") + nocdata: true, + recover: config.get("XML_RECOVER"), + ignore_enc: config.get("XML_IGNORE_ENC") }; const LIBXMLJS_SAVE_OPTIONS = { - "format": config.get("XML_FORMAT"), - "declaration": !config.get("XML_NO_DECL"), - "selfCloseEmpty": !config.get("XML_NO_EMPTY") + format: config.get("XML_FORMAT"), + declaration: !config.get("XML_NO_DECL"), + selfCloseEmpty: !config.get("XML_NO_EMPTY") }; const XML_IGNORE_NAMESPACE = config.get("XML_IGNORE_NAMESPACE"); @@ -79,32 +79,29 @@ const XML_IGNORE_NAMESPACE = config.get("XML_IGNORE_NAMESPACE"); if (XML_IGNORE_NAMESPACE) { libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find; libxmljs.Element.prototype.find = function(xpath, namespaces) { - const p = xpath.replace(/([^\/:]+:)?([^\/]+)(\/|$)/g, "$2$3").replace(/([a-zA-Z0-9_-]+)([^\/]*)(\/|$)/g, "*[local-name()=\"$1\"]$2$3"); + const p = xpath + .replace(/([^/:]+:)?([^/]+)(\/|$)/g, "$2$3") + .replace(/([a-zA-Z0-9_-]+)([^/]*)(\/|$)/g, '*[local-name()="$1"]$2$3'); return libxmljs.Element.prototype.__find.call(this, p, namespaces); }; } let warnings; - function parseBool(v) { v = "" + v; - if (v === "true" || v === "TRUE" || v === "True" || v === "1") - return true; + if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true; else if (v === "false" || v === "FALSE" || v === "False" || v === "0") return false; - else - return null; + else return null; } - function event(xml) { return xml.find("EventStruct/EventCode").map(e => e.text().trim()); } - function parameterInfoList(xml) { - return xml.find("ParameterInfoStruct").map(function(e) { + return xml.find("ParameterInfoStruct").map(e => { let param = e.get("Name").text(); let value = e.get("Writable").text(); let parsed = parseBool(value); @@ -121,9 +118,8 @@ function parameterInfoList(xml) { }); } - function parameterValueList(xml) { - return xml.find("ParameterValueStruct").map(function(e) { + return xml.find("ParameterValueStruct").map(e => { const valueElement = e.get("Value"); const valueType = valueElement.attr("type").value(); let param = e.get("Name").text(); @@ -138,8 +134,7 @@ function parameterValueList(xml) { }); parsed = value; } - } - else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") { + } else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") { parsed = parseInt(value); if (isNaN(parsed)) { warnings.push({ @@ -148,8 +143,7 @@ function parameterValueList(xml) { }); parsed = value; } - } - else if (valueType === "xsd:dateTime") { + } else if (valueType === "xsd:dateTime") { parsed = Date.parse(value); if (isNaN(parsed)) { warnings.push({ @@ -160,18 +154,16 @@ function parameterValueList(xml) { } } - return [param, parsed, valueType]; + return [param, parsed, valueType]; }); } - function GetParameterNames(xml, methodRequest) { const el = xml.node("cwmp:GetParameterNames"); el.node("ParameterPath").text(methodRequest.parameterPath); el.node("NextLevel").text(+methodRequest.nextLevel); } - function GetParameterNamesResponse(xml) { return { name: "GetParameterNamesResponse", @@ -179,18 +171,15 @@ function GetParameterNamesResponse(xml) { }; } - function GetParameterValues(xml, methodRequest) { const el = xml.node("cwmp:GetParameterValues").node("ParameterNames"); el.attr({ "soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]` }); - for (const p of methodRequest.parameterNames) - el.node("string").text(p); + for (const p of methodRequest.parameterNames) el.node("string").text(p); } - function GetParameterValuesResponse(xml) { return { name: "GetParameterValuesResponse", @@ -198,34 +187,36 @@ function GetParameterValuesResponse(xml) { }; } - function SetParameterValues(xml, methodRequest) { const el = xml.node("cwmp:SetParameterValues"); const paramList = el.node("ParameterList"); paramList.attr({ - "soap-enc:arrayType": `cwmp:ParameterValueStruct[${methodRequest.parameterList.length}]` + "soap-enc:arrayType": `cwmp:ParameterValueStruct[${ + methodRequest.parameterList.length + }]` }); for (const p of methodRequest.parameterList) { const pvs = paramList.node("ParameterValueStruct"); pvs.node("Name").text(p[0]); let val = p[1]; - if (p[2] === 'xsd:dateTime' && typeof val === 'number') { - val = (new Date(val)).toISOString(); + if (p[2] === "xsd:dateTime" && typeof val === "number") { + val = new Date(val).toISOString(); if (methodRequest.DATETIME_MILLISECONDS === false) - val = val.replace('.000', ''); + val = val.replace(".000", ""); } - if (p[2] === 'xsd:boolean' && typeof val === 'boolean') { - if (methodRequest.BOOLEAN_LITERAL === false) - val = +val; - } - pvs.node("Value").attr({"xsi:type": p[2]}).text(val); + if (p[2] === "xsd:boolean" && typeof val === "boolean") + if (methodRequest.BOOLEAN_LITERAL === false) val = +val; + + pvs + .node("Value") + .attr({ "xsi:type": p[2] }) + .text(val); } el.node("ParameterKey").text(methodRequest.parameterKey || ""); } - function SetParameterValuesResponse(xml) { return { name: "SetParameterValuesResponse", @@ -233,14 +224,12 @@ function SetParameterValuesResponse(xml) { }; } - function AddObject(xml, methodRequest) { const el = xml.node("cwmp:AddObject"); el.node("ObjectName").text(methodRequest.objectName); el.node("ParameterKey").text(methodRequest.parameterKey || ""); } - function AddObjectResponse(xml) { return { name: "AddObjectResponse", @@ -249,14 +238,12 @@ function AddObjectResponse(xml) { }; } - function DeleteObject(xml, methodRequest) { const el = xml.node("cwmp:DeleteObject"); el.node("ObjectName").text(methodRequest.objectName); el.node("ParameterKey").text(methodRequest.parameterKey || ""); } - function DeleteObjectResponse(xml) { return { name: "DeleteObjectResponse", @@ -264,32 +251,27 @@ function DeleteObjectResponse(xml) { }; } - function Reboot(xml, methodRequest) { const el = xml.node("cwmp:Reboot"); el.node("CommandKey").text(methodRequest.commandKey || ""); } - -function RebootResponse(xml) { +function RebootResponse() { return { name: "RebootResponse" }; } - -function FactoryReset(xml, methodRequest) { +function FactoryReset(xml) { xml.node("cwmp:FactoryReset"); } - -function FactoryResetResponse(xml, methodRequest) { +function FactoryResetResponse() { return { name: "FactoryResetResponse" }; } - function Download(xml, methodRequest) { const el = xml.node("cwmp:Download"); el.node("CommandKey").text(methodRequest.commandKey || ""); @@ -304,7 +286,6 @@ function Download(xml, methodRequest) { el.node("FailureURL").text(methodRequest.failureUrl || ""); } - function DownloadResponse(xml) { const res = { name: "DownloadResponse", @@ -319,44 +300,41 @@ function DownloadResponse(xml) { return res; } - function Inform(xml) { return { name: "Inform", parameterList: parameterValueList(xml.get("ParameterList")), deviceId: { - "Manufacturer": xml.get("DeviceId/Manufacturer").text(), - "OUI": xml.get("DeviceId/OUI").text(), - "ProductClass": xml.get("DeviceId/ProductClass").text(), - "SerialNumber": xml.get("DeviceId/SerialNumber").text() + Manufacturer: xml.get("DeviceId/Manufacturer").text(), + OUI: xml.get("DeviceId/OUI").text(), + ProductClass: xml.get("DeviceId/ProductClass").text(), + SerialNumber: xml.get("DeviceId/SerialNumber").text() }, event: event(xml.get("Event")), retryCount: parseInt(xml.get("RetryCount").text()) }; } - -function InformResponse(xml, methodResponse) { - xml.node("cwmp:InformResponse").node("MaxEnvelopes").text(1); +function InformResponse(xml) { + xml + .node("cwmp:InformResponse") + .node("MaxEnvelopes") + .text(1); } - -function GetRPCMethods(xml) { - return {name: "GetRPCMethods"}; +function GetRPCMethods() { + return { name: "GetRPCMethods" }; } - function GetRPCMethodsResponse(xml, methodResponse) { const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList"); el.attr({ "soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]` }); - for (const m of methodResponse.methodList) - el.node("string").text(m); + for (const m of methodResponse.methodList) el.node("string").text(m); } - function TransferComplete(xml) { return { name: "TransferComplete", @@ -367,12 +345,10 @@ function TransferComplete(xml) { }; } - -function TransferCompleteResponse(xml, methodResponse) { +function TransferCompleteResponse(xml) { xml.node("cwmp:TransferCompleteResponse").text(""); } - function RequestDownload(xml) { return { name: "RequestDownload", @@ -380,12 +356,10 @@ function RequestDownload(xml) { }; } - -function RequestDownloadResponse(xml, methodResponse) { +function RequestDownloadResponse(xml) { xml.node("cwmp:RequestDownloadResponse").text(""); } - function faultStruct(xml) { const f = { faultCode: xml.get("FaultCode").text(), @@ -393,8 +367,7 @@ function faultStruct(xml) { }; for (const e of xml.find("SetParameterValuesFault")) { - if (!f.setParameterValuesFault) - f.setParameterValuesFault = []; + if (!f.setParameterValuesFault) f.setParameterValuesFault = []; f.setParameterValuesFault.push({ parameterName: e.get("ParameterName").text(), @@ -406,7 +379,6 @@ function faultStruct(xml) { return f; } - function fault(xml, cwmpVersion) { return { faultCode: xml.get("faultcode").text(), @@ -415,7 +387,6 @@ function fault(xml, cwmpVersion) { }; } - function request(data, cwmpVersion, warn) { warnings = warn; @@ -423,19 +394,24 @@ function request(data, cwmpVersion, warn) { cwmpVersion: cwmpVersion }; - if (!data.length) - return rpc; + if (!data.length) return rpc; const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS); let methodElement; if (rpc.cwmpVersion) { - methodElement = xml.get("/soap-env:Envelope/soap-env:Body/cwmp:*", NAMESPACES[rpc.cwmpVersion]); - } - else { + methodElement = xml.get( + "/soap-env:Envelope/soap-env:Body/cwmp:*", + NAMESPACES[rpc.cwmpVersion] + ); + } else { // cwmpVersion not passed, thus it's an inform request - methodElement = xml.get("/soap-env:Envelope/soap-env:Body/*", NAMESPACES["1.0"]); + methodElement = xml.get( + "/soap-env:Envelope/soap-env:Body/*", + NAMESPACES["1.0"] + ); + let timeoutElement; switch (methodElement.namespace().href()) { case "urn:dslforum-org:cwmp-1-0": rpc.cwmpVersion = "1.0"; @@ -444,12 +420,14 @@ function request(data, cwmpVersion, warn) { rpc.cwmpVersion = "1.1"; break; case "urn:dslforum-org:cwmp-1-2": - const timeoutElement = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", NAMESPACES["1.2"]); + timeoutElement = xml.get( + "/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", + NAMESPACES["1.2"] + ); if (timeoutElement) { rpc.sessionTimeout = parseInt(timeoutElement.text()); rpc.cwmpVersion = "1.3"; - } - else { + } else { rpc.cwmpVersion = "1.2"; } break; @@ -461,17 +439,24 @@ function request(data, cwmpVersion, warn) { } if (!rpc.sessionTimeout) { - const timeoutElement = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", NAMESPACES[rpc.cwmpVersion]); - if (timeoutElement) - rpc.sessionTimeout = parseInt(timeoutElement.text()); + timeoutElement = xml.get( + "/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", + NAMESPACES[rpc.cwmpVersion] + ); + if (timeoutElement) rpc.sessionTimeout = parseInt(timeoutElement.text()); } } - let idElement = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:ID", NAMESPACES[rpc.cwmpVersion]); - if (idElement) - rpc.id = idElement.text(); + let idElement = xml.get( + "/soap-env:Envelope/soap-env:Header/cwmp:ID", + NAMESPACES[rpc.cwmpVersion] + ); + if (idElement) rpc.id = idElement.text(); - if (methodElement && !(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault")) { + if ( + methodElement && + !(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault") + ) { switch (methodElement.name()) { case "Inform": rpc.cpeRequest = Inform(methodElement); @@ -512,16 +497,17 @@ function request(data, cwmpVersion, warn) { default: throw new Error(`8000 Method not supported ${methodElement.name()}`); } - } - else { - const faultElement = xml.get("/soap-env:Envelope/soap-env:Body/soap-env:Fault", NAMESPACES[rpc.cwmpVersion]); + } else { + const faultElement = xml.get( + "/soap-env:Envelope/soap-env:Body/soap-env:Fault", + NAMESPACES[rpc.cwmpVersion] + ); rpc.cpeFault = fault(faultElement, rpc.cwmpVersion); } return rpc; } - function createSoapEnv(cwmpVersion) { const xml = libxmljs.Document(); const env = xml.node("soap-env:Envelope"); @@ -532,21 +518,22 @@ function createSoapEnv(cwmpVersion) { return env; } - function response(rpc) { const headers = { - "Server": SERVER_NAME, - "SOAPServer": SERVER_NAME + Server: SERVER_NAME, + SOAPServer: SERVER_NAME }; - if (!rpc) - return {code: 204, headers: headers, data: new Buffer(0)}; + if (!rpc) return { code: 204, headers: headers, data: new Buffer(0) }; const env = createSoapEnv(rpc.cwmpVersion); const header = env.node("soap-env:Header"); - header.node("cwmp:ID").attr({ - "soap-env:mustUnderstand": 1 - }).text(rpc.id); + header + .node("cwmp:ID") + .attr({ + "soap-env:mustUnderstand": 1 + }) + .text(rpc.id); const body = env.node("soap-env:Body"); @@ -567,8 +554,7 @@ function response(rpc) { default: throw new Error(`Unknown method response type ${rpc.acsResponse.name}`); } - } - else if (rpc.acsRequest) { + } else if (rpc.acsRequest) { switch (rpc.acsRequest.name) { case "GetParameterNames": GetParameterNames(body, rpc.acsRequest); @@ -599,7 +585,7 @@ function response(rpc) { } } - headers["Content-Type"] = "text/xml; charset=\"utf-8\""; + headers["Content-Type"] = 'text/xml; charset="utf-8"'; return { code: 200, headers: headers, @@ -607,6 +593,5 @@ function response(rpc) { }; } - exports.request = request; exports.response = response; diff --git a/lib/versioned-map.js b/lib/versioned-map.js index 2a6de6d..1b6f599 100644 --- a/lib/versioned-map.js +++ b/lib/versioned-map.js @@ -1,5 +1,5 @@ /** - * Copyright 2013-2017 Zaid Abdulla + * Copyright 2013-2018 Zaid Abdulla * * This file is part of GenieACS. * @@ -20,18 +20,21 @@ const NONEXISTENT = new Object(); - function compareEquality(a, b) { var t = typeof a; - if (t === 'number' || (a == null) || t === 'boolean' || t === 'string' || t === 'symbol') + if ( + t === "number" || + a == null || + t === "boolean" || + t === "string" || + t === "symbol" + ) return a === b; return JSON.stringify(a) === JSON.stringify(b); } - class VersionedMap { - constructor() { this._sizeDiff = [0]; this._revision = 0; @@ -39,71 +42,59 @@ class VersionedMap { this.dirty = 0; } - get size() { return this.map.size + this._sizeDiff[this.revision]; } - get revision() { return this._revision; } - set revision(rev) { - for (let i = this._sizeDiff.length; i <= rev; ++ i) + for (let i = this._sizeDiff.length; i <= rev; ++i) this._sizeDiff[i] = this._sizeDiff[i - 1]; this._revision = rev; } - get(key, rev) { - if (rev == null) - rev = this._revision + if (rev == null) rev = this._revision; var v = this.map.get(key); - if (!v) - return; + if (!v) return; - for (let i = Math.min(v.length - 1, rev); i >= 0; -- i) - if (i in v) - if (v[i] === NONEXISTENT) - return undefined; - else - return v[i]; + for (let i = Math.min(v.length - 1, rev); i >= 0; --i) { + if (i in v) { + if (v[i] === NONEXISTENT) return undefined; + else return v[i]; + } + } } - has(key, rev) { - if (rev == null) - rev = this._revision + if (rev == null) rev = this._revision; var v = this.map.get(key); - if (v == null) - return false; + if (v == null) return false; - for (let i = Math.min(v.length - 1, rev); i >= 0; -- i) - if (i in v) - if (v[i] === NONEXISTENT) - return false; - else - return true; + for (let i = Math.min(v.length - 1, rev); i >= 0; --i) { + if (i in v) { + if (v[i] === NONEXISTENT) return false; + else return true; + } + } return false; } - set(key, value, rev) { - if (rev == null) - rev = this._revision; + if (rev == null) rev = this._revision; var v = this.map.get(key); if (!v) { this.dirty |= 1 << rev; - for (let i = 0; i < rev; ++ i) - this._sizeDiff[i] -= 1; + for (let i = 0; i < rev; ++i) this._sizeDiff[i] -= 1; v = []; v[rev] = value; @@ -114,71 +105,59 @@ class VersionedMap { var old = v[Math.min(rev, v.length - 1)]; if (rev < v.length - 1) { - if (compareEquality(value, old)) - return - else - throw new Error('Cannot modify old revisions'); + if (compareEquality(value, old)) return; + else throw new Error("Cannot modify old revisions"); } this.dirty |= 1 << rev; - if (old === NONEXISTENT) - ++ this._sizeDiff[rev]; + if (old === NONEXISTENT) ++this._sizeDiff[rev]; v[rev] = value; } - delete(key, rev) { - if (rev == null) - rev = this._revision; + if (rev == null) rev = this._revision; var v = this.map.get(key); - if (!v) - return false; + if (!v) return false; - if (rev < v.length - 1) - throw new Error('Cannot modify old revisions'); + if (rev < v.length - 1) throw new Error("Cannot modify old revisions"); var old = v[v.length - 1]; - if (old === NONEXISTENT) - return false; + if (old === NONEXISTENT) return false; this.dirty |= 1 << rev; - -- this._sizeDiff[rev]; + --this._sizeDiff[rev]; v[rev] = NONEXISTENT; return true; } - getRevisions(key) { var v = this.map.get(key); - if (!v) - return null; + if (!v) return null; var res = {}; - for (let i in v) - if (v[i] === NONEXISTENT) - res.delete |= 1 << i; - else - res[i] = v[i]; + for (let i in v) { + if (v[i] === NONEXISTENT) res.delete |= 1 << i; + else res[i] = v[i]; + } return res; } - setRevisions(key, revisions) { var del = 0; var rev = []; var minKey = 999; for (let k in revisions) { - if (k === 'delete') + if (k === "delete") { del = revisions[k]; - else { + } else { let r = parseInt(k); minKey = Math.min(minKey, r); - for (let i = this._sizeDiff.length; i <= r; ++ i) + for (let i = this._sizeDiff.length; i <= r; ++i) this._sizeDiff[i] = this._sizeDiff[i - 1]; this.dirty |= 1 << r; @@ -186,63 +165,57 @@ class VersionedMap { } } - for (let i = 0; i < minKey; ++ i) - -- this._sizeDiff[i]; + for (let i = 0; i < minKey; ++i) --this._sizeDiff[i]; this.dirty |= del; - for (let i = 0; del > 0; del >>= 1, ++ i) - if (del & 1) - rev[i] = NONEXISTENT; + for (let i = 0; del > 0; del >>= 1, ++i) if (del & 1) rev[i] = NONEXISTENT; this.map.set(key, rev); } - getDiff(key) { var revisions = this.map.get(key); - if (!revisions) - return []; + if (!revisions) return []; var current = revisions[revisions.length - 1]; if (current === NONEXISTENT) { - if (0 in revisions) - return [key, revisions[0]]; - else - return [key]; - } else if (0 in revisions) + if (0 in revisions) return [key, revisions[0]]; + else return [key]; + } else if (0 in revisions) { return [key, revisions[0], current]; - else + } else { + // TODO don't use sparse arrays? + // eslint-disable-next-line no-sparse-arrays return [key, , current]; + } } - *diff() { for (let pair of this.map) { let current = pair[1][pair[1].length - 1]; if (current === NONEXISTENT) { - if (0 in pair[1]) - yield [pair[0], pair[1][0]]; - else - yield [pair[0]]; - } else if (0 in pair[1]) + if (0 in pair[1]) yield [pair[0], pair[1][0]]; + else yield [pair[0]]; + } else if (0 in pair[1]) { yield [pair[0], pair[1][0], current]; - else + } else { + // TODO don't use sparse arrays? + // eslint-disable-next-line no-sparse-arrays yield [pair[0], , current]; + } } } - collapse(revision) { - if (this._sizeDiff.length <= revision) - return; + if (this._sizeDiff.length <= revision) return; this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1]; this._sizeDiff.splice(revision + 1, this._sizeDiff.length); let d = this.dirty >> revision; this.dirty = this.dirty ^ (d << revision); - this.dirty |= (+!!d) << revision; + this.dirty |= +!!d << revision; for (let pair of this.map) { let k = pair[0]; @@ -250,18 +223,14 @@ class VersionedMap { let last = v[v.length - 1]; if (last === NONEXISTENT) { v.splice(revision, v.length); - if (v.some(function(val) { return true; })) - v[revision] = NONEXISTENT; - else - this.map.delete(k); - } - else { + if (v.some(() => true)) v[revision] = NONEXISTENT; + else this.map.delete(k); + } else { v[revision] = last; v.splice(revision + 1, v.length); } } } - } module.exports = VersionedMap; diff --git a/package.json b/package.json index 97ea258..74f823a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "redis": "^2.6.5" }, "devDependencies": { + "eslint": "^5.4.0", + "prettier": "^1.14.2" }, "engines": { "node": ">=6.9.2 <9.0" @@ -36,6 +38,7 @@ "compile": "echo 'Compile step no longer required, skipping.'", "configure": "cp -n ./config/config-sample.json ./config/config.json", "install": "npm run configure", - "test": "node test" + "test": "node test", + "lint": "eslint --fix lib/*.js; prettier --write lib/*.js" } }